From f675604864589a4717eb6cfb8bf41456e48b27ca Mon Sep 17 00:00:00 2001 From: oxy Date: Sun, 9 Jun 2024 16:21:11 +0800 Subject: [PATCH] New Release Card. --- .../m3u/core/architecture/logger/Profiles.kt | 1 + .../main/java/com/m3u/core/unit/DataUnit.kt | 14 ++- .../m3u/data/repository/RepositoryModule.kt | 8 ++ .../repository/media/MediaRepositoryImpl.kt | 2 +- .../data/repository/other/OtherRepository.kt | 7 ++ .../repository/other/OtherRepositoryImpl.kt | 30 +++++++ .../favorite/components/FavoriteGallery.kt | 42 +-------- .../com/m3u/feature/foryou/ForyouScreen.kt | 19 ++-- .../com/m3u/feature/foryou/ForyouViewModel.kt | 39 ++++++-- .../foryou/components/recommend/Recommend.kt | 10 +++ .../components/recommend/RecommendGallery.kt | 51 ++++++----- .../components/recommend/RecommendItem.kt | 88 +++++++++++++++++++ i18n/src/main/res/values/feat_foryou.xml | 1 + ui/src/main/java/com/m3u/ui/Brushes.kt | 43 +++++++++ 14 files changed, 276 insertions(+), 79 deletions(-) create mode 100644 data/src/main/java/com/m3u/data/repository/other/OtherRepository.kt create mode 100644 data/src/main/java/com/m3u/data/repository/other/OtherRepositoryImpl.kt create mode 100644 ui/src/main/java/com/m3u/ui/Brushes.kt diff --git a/core/src/main/java/com/m3u/core/architecture/logger/Profiles.kt b/core/src/main/java/com/m3u/core/architecture/logger/Profiles.kt index 28c8b2f6a..e544dae04 100644 --- a/core/src/main/java/com/m3u/core/architecture/logger/Profiles.kt +++ b/core/src/main/java/com/m3u/core/architecture/logger/Profiles.kt @@ -15,6 +15,7 @@ object Profiles { val REPOS_PROGRAMME = Profile("repos-programme") val REPOS_TELEVISION = Profile("repos-television") val REPOS_MEDIA = Profile("repos-media") + val REPOS_OTHER = Profile("repos-other") val PARSER_M3U = Profile("parser-m3u") val PARSER_XTREAM = Profile("parser-xtream") diff --git a/core/src/main/java/com/m3u/core/unit/DataUnit.kt b/core/src/main/java/com/m3u/core/unit/DataUnit.kt index d6d4e5ce0..e7b490520 100644 --- a/core/src/main/java/com/m3u/core/unit/DataUnit.kt +++ b/core/src/main/java/com/m3u/core/unit/DataUnit.kt @@ -9,7 +9,7 @@ val Double.MB: DataUnit.MB get() = DataUnit.MB(this) val Double.KB: DataUnit.KB get() = DataUnit.KB(this) @Immutable -sealed class DataUnit { +sealed class DataUnit : Comparable { data class GB(val value: Double) : DataUnit() { override fun toString(): String = "${value.toInt()} GB" } @@ -22,6 +22,18 @@ sealed class DataUnit { override fun toString(): String = "${value.toInt()} KB" } + val length: Double + get() = when (this) { + is GB -> 1024 * 1024 * 1024 * value + is MB -> 1024 * 1024 * value + is KB -> 1024 * value + Unspecified -> 0.0 + } + + override fun compareTo(other: DataUnit): Int { + return this.length.compareTo(other.length) + } + companion object { private const val KB: Long = 1024 private const val MB = KB * 1024 diff --git a/data/src/main/java/com/m3u/data/repository/RepositoryModule.kt b/data/src/main/java/com/m3u/data/repository/RepositoryModule.kt index f4783dd50..d438ac85d 100644 --- a/data/src/main/java/com/m3u/data/repository/RepositoryModule.kt +++ b/data/src/main/java/com/m3u/data/repository/RepositoryModule.kt @@ -11,6 +11,8 @@ import com.m3u.data.repository.media.MediaRepositoryImpl import com.m3u.data.repository.playlist.PlaylistRepositoryImpl import com.m3u.data.repository.programme.ProgrammeRepositoryImpl import com.m3u.data.repository.channel.ChannelRepositoryImpl +import com.m3u.data.repository.other.OtherRepository +import com.m3u.data.repository.other.OtherRepositoryImpl import com.m3u.data.repository.television.TelevisionRepositoryImpl import dagger.Binds import dagger.Module @@ -50,4 +52,10 @@ internal interface RepositoryModule { fun bindTelevisionRepository( repository: TelevisionRepositoryImpl ): TelevisionRepository + + @Binds + @Singleton + fun bindOtherRepository( + repositoryImpl: OtherRepositoryImpl + ): OtherRepository } diff --git a/data/src/main/java/com/m3u/data/repository/media/MediaRepositoryImpl.kt b/data/src/main/java/com/m3u/data/repository/media/MediaRepositoryImpl.kt index 50f089509..b688d06b3 100644 --- a/data/src/main/java/com/m3u/data/repository/media/MediaRepositoryImpl.kt +++ b/data/src/main/java/com/m3u/data/repository/media/MediaRepositoryImpl.kt @@ -32,7 +32,7 @@ import javax.inject.Inject private const val BITMAP_QUALITY = 100 -class MediaRepositoryImpl @Inject constructor( +internal class MediaRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, delegate: Logger, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher diff --git a/data/src/main/java/com/m3u/data/repository/other/OtherRepository.kt b/data/src/main/java/com/m3u/data/repository/other/OtherRepository.kt new file mode 100644 index 000000000..a0c67a6ac --- /dev/null +++ b/data/src/main/java/com/m3u/data/repository/other/OtherRepository.kt @@ -0,0 +1,7 @@ +package com.m3u.data.repository.other + +import com.m3u.data.api.dto.github.Release + +interface OtherRepository { + suspend fun getRelease(): Release? +} \ No newline at end of file diff --git a/data/src/main/java/com/m3u/data/repository/other/OtherRepositoryImpl.kt b/data/src/main/java/com/m3u/data/repository/other/OtherRepositoryImpl.kt new file mode 100644 index 000000000..0850f033f --- /dev/null +++ b/data/src/main/java/com/m3u/data/repository/other/OtherRepositoryImpl.kt @@ -0,0 +1,30 @@ +package com.m3u.data.repository.other + +import com.m3u.core.architecture.Publisher +import com.m3u.core.architecture.logger.Logger +import com.m3u.core.architecture.logger.Profiles +import com.m3u.core.architecture.logger.execute +import com.m3u.core.architecture.logger.install +import com.m3u.core.util.collections.indexOf +import com.m3u.data.api.GithubApi +import com.m3u.data.api.dto.github.Release +import javax.inject.Inject + +internal class OtherRepositoryImpl @Inject constructor( + private val githubApi: GithubApi, + private val publisher: Publisher, + delegate: Logger +) : OtherRepository { + private val logger = delegate.install(Profiles.REPOS_OTHER) + override suspend fun getRelease(): Release? { + if (publisher.snapshot) return null + val versionName = publisher.versionName + val releases = logger + .execute { githubApi.releases("oxyroid", "M3UAndroid") } + ?: emptyList() + if (releases.isEmpty()) return null + val i = releases.indexOf { it.name == versionName } +// if (i <= 0) return null + return releases.first() + } +} \ No newline at end of file diff --git a/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt b/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt index 4269a9fba..63f49228f 100644 --- a/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt +++ b/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt @@ -1,11 +1,5 @@ package com.m3u.feature.favorite.components -import androidx.compose.animation.animateColor -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -25,7 +19,6 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush @@ -40,6 +33,7 @@ import com.m3u.i18n.R.string import com.m3u.material.ktx.isTelevision import com.m3u.material.ktx.plus import com.m3u.material.model.LocalSpacing +import com.m3u.ui.createPremiumBrush import androidx.tv.material3.Card as TvCard import androidx.tv.material3.CardDefaults as TvCardDefaults import androidx.tv.material3.Glow as TvGlow @@ -131,7 +125,7 @@ private fun RandomTips( .clip(AbsoluteRoundedCornerShape(spacing.medium)) .clickable(onClick = onClick) .background( - createPremiumBrush( + Brush.createPremiumBrush( MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.tertiary ) @@ -156,7 +150,7 @@ private fun RandomTips( modifier = Modifier .fillMaxWidth() .background( - createPremiumBrush( + Brush.createPremiumBrush( TvMaterialTheme.colorScheme.primary, TvMaterialTheme.colorScheme.tertiary ) @@ -175,33 +169,3 @@ private fun RandomTips( } } } - -@Composable -private fun createPremiumBrush( - color1: Color = MaterialTheme.colorScheme.primaryContainer, - color2: Color = MaterialTheme.colorScheme.secondaryContainer -): Brush { - val transition = rememberInfiniteTransition("premium-brush") - - val leftColor by transition.animateColor( - initialValue = color1, - targetValue = color2, - animationSpec = infiniteRepeatable( - animation = tween(1600, easing = LinearEasing), - repeatMode = RepeatMode.Reverse - ), - label = "left" - ) - val rightColor by transition.animateColor( - initialValue = color2, - targetValue = color1, - animationSpec = infiniteRepeatable( - animation = tween(1600, easing = LinearEasing), - repeatMode = RepeatMode.Reverse - ), - label = "right" - ) - return Brush.linearGradient( - colors = listOf(leftColor, rightColor) - ) -} \ No newline at end of file diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt index 387664b82..da2cdb01a 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt @@ -34,9 +34,9 @@ import androidx.lifecycle.repeatOnLifecycle import com.m3u.core.architecture.preferences.hiltPreferences import com.m3u.core.util.basic.title import com.m3u.core.wrapper.Resource +import com.m3u.data.database.model.Channel import com.m3u.data.database.model.Playlist import com.m3u.data.database.model.PlaylistWithCount -import com.m3u.data.database.model.Channel import com.m3u.data.database.model.isSeries import com.m3u.data.service.MediaCommand import com.m3u.feature.foryou.components.HeadlineBackground @@ -81,7 +81,7 @@ fun ForyouRoute( val isPageInfoVisible = root == Destination.Root.Foryou val playlistCountsResource by viewModel.playlistCountsResource.collectAsStateWithLifecycle() - val recommend by viewModel.recommend.collectAsStateWithLifecycle() + val specs by viewModel.specs.collectAsStateWithLifecycle() val episodes by viewModel.episodes.collectAsStateWithLifecycle() val series: Channel? by viewModel.series.collectAsStateWithLifecycle() @@ -112,7 +112,7 @@ fun ForyouRoute( playlistCountsResource = playlistCountsResource, subscribingPlaylistUrls = subscribingPlaylistUrls, refreshingEpgUrls = refreshingEpgUrls, - recommend = recommend, + specs = specs, rowCount = preferences.rowCount, contentPadding = contentPadding, navigateToPlaylist = navigateToPlaylist, @@ -177,7 +177,7 @@ private fun ForyouScreen( playlistCountsResource: Resource>, subscribingPlaylistUrls: List, refreshingEpgUrls: List, - recommend: Recommend, + specs: List, contentPadding: PaddingValues, navigateToPlaylist: (Playlist) -> Unit, onClickChannel: (Channel) -> Unit, @@ -202,12 +202,13 @@ private fun ForyouScreen( } LaunchedEffect(headlineSpec) { - val currentHeadlineSpec = headlineSpec + val spec = headlineSpec lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { delay(400.milliseconds) - Metadata.headlineUrl = when (currentHeadlineSpec) { - is Recommend.UnseenSpec -> currentHeadlineSpec.channel.cover.orEmpty() + Metadata.headlineUrl = when (spec) { + is Recommend.UnseenSpec -> spec.channel.cover.orEmpty() is Recommend.DiscoverSpec -> "" + is Recommend.NewRelease -> "" else -> "" } } @@ -228,7 +229,7 @@ private fun ForyouScreen( val showPlaylist = playlistCountsResource.data.isNotEmpty() val header = @Composable { RecommendGallery( - recommend = recommend, + specs = specs, navigateToPlaylist = navigateToPlaylist, onClickChannel = onClickChannel, onSpecChanged = { spec -> headlineSpec = spec }, @@ -243,7 +244,7 @@ private fun ForyouScreen( refreshingEpgUrls = refreshingEpgUrls, onClick = navigateToPlaylist, onLongClick = { mediaSheetValue = MediaSheetValue.ForyouScreen(it) }, - header = header.takeIf { recommend.isNotEmpty() }, + header = header.takeIf { specs.isNotEmpty() }, contentPadding = contentPadding, modifier = Modifier.fillMaxSize() ) diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouViewModel.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouViewModel.kt index 81552fb61..dba6f8d91 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouViewModel.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouViewModel.kt @@ -12,17 +12,20 @@ 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.architecture.preferences.Preferences +import com.m3u.core.unit.DataUnit import com.m3u.core.wrapper.Resource import com.m3u.core.wrapper.asResource import com.m3u.core.wrapper.mapResource import com.m3u.core.wrapper.resource +import com.m3u.data.api.dto.github.Release +import com.m3u.data.database.model.Channel import com.m3u.data.database.model.Playlist import com.m3u.data.database.model.PlaylistWithCount -import com.m3u.data.database.model.Channel import com.m3u.data.parser.xtream.XtreamChannelInfo +import com.m3u.data.repository.channel.ChannelRepository +import com.m3u.data.repository.other.OtherRepository import com.m3u.data.repository.playlist.PlaylistRepository import com.m3u.data.repository.programme.ProgrammeRepository -import com.m3u.data.repository.channel.ChannelRepository import com.m3u.data.worker.SubscriptionWorker import com.m3u.feature.foryou.components.recommend.Recommend import dagger.hilt.android.lifecycle.HiltViewModel @@ -48,6 +51,7 @@ class ForyouViewModel @Inject constructor( private val playlistRepository: PlaylistRepository, channelRepository: ChannelRepository, programmeRepository: ProgrammeRepository, + otherRepository: OtherRepository, preferences: Preferences, @Dispatcher(IO) ioDispatcher: CoroutineDispatcher, workManager: WorkManager, @@ -95,14 +99,39 @@ class ForyouViewModel @Inject constructor( initialValue = Duration.INFINITE ) - internal val recommend: StateFlow = unseensDuration + private val newRelease: StateFlow = flow { + emit(otherRepository.getRelease()) + } + .stateIn( + scope = viewModelScope, + initialValue = null, + started = SharingStarted.Lazily + ) + internal val specs: StateFlow> = unseensDuration .flatMapLatest { channelRepository.observeAllUnseenFavourites(it) } - .map { prev -> Recommend(prev.map { Recommend.UnseenSpec(it) }) } + .let { flow -> + combine(flow, newRelease) { channels, nr -> + buildList { + if (nr != null) { + val min = DataUnit.of(nr.assets.minOfOrNull { it.size }?.toLong() ?: 0L) + val max = DataUnit.of(nr.assets.maxOfOrNull { it.size }?.toLong() ?: 0L) + this += Recommend.NewRelease( + name = nr.name, + description = nr.body, + downloadCount = nr.assets.sumOf { it.downloadCount }, + size = min..max, + url = nr.htmlUrl + ) + } + this += channels.map { channel -> Recommend.UnseenSpec(channel) } + } + } + } .flowOn(ioDispatcher) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), - initialValue = Recommend() + initialValue = emptyList() ) internal fun onUnsubscribePlaylist(url: String) { diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/Recommend.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/Recommend.kt index 2ce3d67ca..8ccab10ed 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/Recommend.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/Recommend.kt @@ -1,6 +1,7 @@ package com.m3u.feature.foryou.components.recommend import androidx.compose.runtime.Immutable +import com.m3u.core.unit.DataUnit import com.m3u.data.database.model.Playlist import com.m3u.data.database.model.Channel @@ -25,4 +26,13 @@ internal class Recommend( data class UnseenSpec( val channel: Channel ) : Spec + + @Immutable + data class NewRelease( + val name: String, + val description: String, + val downloadCount: Int, + val size: ClosedRange, + val url: String, + ): Spec } diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt index 626577373..059d61c1e 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler import com.m3u.core.wrapper.eventOf import com.m3u.data.database.model.Channel import com.m3u.data.database.model.Playlist @@ -29,45 +30,55 @@ import androidx.tv.material3.Carousel as TvCarousel @Composable internal fun RecommendGallery( - recommend: Recommend, + specs: List, onClickChannel: (Channel) -> Unit, navigateToPlaylist: (Playlist) -> Unit, onSpecChanged: (Recommend.Spec) -> Unit, modifier: Modifier = Modifier ) { val spacing = LocalSpacing.current + val uriHandler = LocalUriHandler.current val tv = isTelevision() + val onClick = { spec: Recommend.Spec -> + when (spec) { + is Recommend.UnseenSpec -> { + onClickChannel(spec.channel) + } + + is Recommend.DiscoverSpec -> { + Events.discoverCategory = eventOf(spec.category) + navigateToPlaylist(spec.playlist) + } + + is Recommend.NewRelease -> { + uriHandler.openUri(spec.url) + } + } + } + if (!tv) { - val state = rememberPagerState { recommend.size } + val state = rememberPagerState { specs.size } Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(spacing.medium) ) { LaunchedEffect(state.currentPage) { - onSpecChanged(recommend[state.currentPage]) + onSpecChanged(specs[state.currentPage]) } HorizontalPager( state = state, contentPadding = PaddingValues(horizontal = spacing.medium), modifier = Modifier.animateContentSize() ) { page -> - val spec = recommend[page] + val spec = specs[page] val pageOffset = ((state.currentPage - page) + state.currentPageOffsetFraction).absoluteValue RecommendItem( spec = spec, pageOffset = pageOffset, - onClick = { - when (spec) { - is Recommend.UnseenSpec -> onClickChannel(spec.channel) - is Recommend.DiscoverSpec -> { - Events.discoverCategory = eventOf(spec.category) - navigateToPlaylist(spec.playlist) - } - } - } + onClick = { onClick(spec) } ) } HorizontalPagerIndicator( @@ -79,7 +90,7 @@ internal fun RecommendGallery( } } else { TvCarousel( - itemCount = recommend.size, + itemCount = specs.size, contentTransformEndToStart = fadeIn(tween(1000)) togetherWith fadeOut(tween(1000)), contentTransformStartToEnd = @@ -88,19 +99,11 @@ internal fun RecommendGallery( .padding(spacing.medium) .then(modifier) ) { index -> - val spec = recommend[index] + val spec = specs[index] RecommendItem( spec = spec, pageOffset = 0f, - onClick = { - when (spec) { - is Recommend.UnseenSpec -> onClickChannel(spec.channel) - is Recommend.DiscoverSpec -> { - Events.discoverCategory = eventOf(spec.category) - navigateToPlaylist(spec.playlist) - } - } - }, + onClick = { onClick(spec) }, modifier = Modifier.animateEnterExit( enter = slideInHorizontally(animationSpec = tween(1000)) { it / 2 }, exit = slideOutHorizontally(animationSpec = tween(1000)) diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt index 6e5965f90..79df60b7b 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt @@ -1,12 +1,20 @@ package com.m3u.feature.foryou.components.recommend +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.Row import androidx.compose.foundation.layout.Spacer 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.material.icons.Icons +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.NewReleases import androidx.compose.material3.Card +import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -16,12 +24,15 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import coil.compose.AsyncImage import coil.request.ImageRequest @@ -31,6 +42,8 @@ import com.m3u.i18n.R.string import com.m3u.material.brush.RecommendCardContainerBrush import com.m3u.material.ktx.isTelevision import com.m3u.material.model.LocalSpacing +import com.m3u.ui.FontFamilies +import com.m3u.ui.createPremiumBrush import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.days @@ -51,6 +64,7 @@ internal fun RecommendItem( when (spec) { is Recommend.UnseenSpec -> UnseenContent(spec) is Recommend.DiscoverSpec -> DiscoverContent(spec) + is Recommend.NewRelease -> NewReleaseContent(spec) } } } @@ -201,4 +215,78 @@ private fun DiscoverContent(spec: Recommend.DiscoverSpec) { fontWeight = FontWeight.Black, maxLines = 1 ) +} + +@Composable +private fun NewReleaseContent(spec: Recommend.NewRelease) { + val spacing = LocalSpacing.current + Row( + Modifier + .fillMaxWidth() + .background( + Brush.createPremiumBrush( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.tertiary + ) + ) + .padding(spacing.medium) + ) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimary) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(string.feat_foryou_new_release).title(), + style = MaterialTheme.typography.titleMedium, + maxLines = 1 + ) + Icon(imageVector = Icons.Rounded.NewReleases, contentDescription = null) + } + Text( + text = spec.name, + style = MaterialTheme.typography.labelLarge, + color = LocalContentColor.current.copy(0.56f), + ) + Text( + text = spec.description, + maxLines = 5, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamilies.LexendExa + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = spec.size.toString(), + style = MaterialTheme.typography.labelMedium, + color = LocalContentColor.current.copy(0.56f) + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = Icons.Rounded.Download, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = LocalContentColor.current.copy(0.56f) + ) + Text( + text = spec.downloadCount.toString(), + style = MaterialTheme.typography.labelMedium, + color = LocalContentColor.current.copy(0.56f) + ) + } + } + } + } + } } \ No newline at end of file diff --git a/i18n/src/main/res/values/feat_foryou.xml b/i18n/src/main/res/values/feat_foryou.xml index b1ee1592b..a4290df85 100644 --- a/i18n/src/main/res/values/feat_foryou.xml +++ b/i18n/src/main/res/values/feat_foryou.xml @@ -10,6 +10,7 @@ more than %d days %d days %d hours + new release enter code from television Make sure to connect to the same Wi-Fi \ No newline at end of file diff --git a/ui/src/main/java/com/m3u/ui/Brushes.kt b/ui/src/main/java/com/m3u/ui/Brushes.kt new file mode 100644 index 000000000..efe1e0aa6 --- /dev/null +++ b/ui/src/main/java/com/m3u/ui/Brushes.kt @@ -0,0 +1,43 @@ +package com.m3u.ui + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +@Composable +fun Brush.Companion.createPremiumBrush( + color1: Color = MaterialTheme.colorScheme.primaryContainer, + color2: Color = MaterialTheme.colorScheme.secondaryContainer +): Brush { + val transition = rememberInfiniteTransition("premium-brush") + + val leftColor by transition.animateColor( + initialValue = color1, + targetValue = color2, + animationSpec = infiniteRepeatable( + animation = tween(1600, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "left" + ) + val rightColor by transition.animateColor( + initialValue = color2, + targetValue = color1, + animationSpec = infiniteRepeatable( + animation = tween(1600, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "right" + ) + return Brush.linearGradient( + colors = listOf(leftColor, rightColor) + ) +} \ No newline at end of file