mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-08-06 14:59:48 +08:00
New Release Card.
This commit is contained in:
@ -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")
|
||||
|
@ -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<DataUnit> {
|
||||
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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.m3u.data.repository.other
|
||||
|
||||
import com.m3u.data.api.dto.github.Release
|
||||
|
||||
interface OtherRepository {
|
||||
suspend fun getRelease(): Release?
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
@ -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<List<PlaylistWithCount>>,
|
||||
subscribingPlaylistUrls: List<String>,
|
||||
refreshingEpgUrls: List<String>,
|
||||
recommend: Recommend,
|
||||
specs: List<Recommend.Spec>,
|
||||
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()
|
||||
)
|
||||
|
@ -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<Recommend> = unseensDuration
|
||||
private val newRelease: StateFlow<Release?> = flow {
|
||||
emit(otherRepository.getRelease())
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = null,
|
||||
started = SharingStarted.Lazily
|
||||
)
|
||||
internal val specs: StateFlow<List<Recommend.Spec>> = unseensDuration
|
||||
.flatMapLatest { channelRepository.observeAllUnseenFavourites(it) }
|
||||
.map { prev -> Recommend(prev.map { Recommend.UnseenSpec(it) }) }
|
||||
.let { flow ->
|
||||
combine(flow, newRelease) { channels, nr ->
|
||||
buildList<Recommend.Spec> {
|
||||
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) {
|
||||
|
@ -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<DataUnit>,
|
||||
val url: String,
|
||||
): Spec
|
||||
}
|
||||
|
@ -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<Recommend.Spec>,
|
||||
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))
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@
|
||||
<string name="feat_foryou_recommend_unseen_more_than_days">more than %d days</string>
|
||||
<string name="feat_foryou_recommend_unseen_days">%d days</string>
|
||||
<string name="feat_foryou_recommend_unseen_hours">%d hours</string>
|
||||
<string name="feat_foryou_new_release">new release</string>
|
||||
<string name="feat_foryou_connect_title">enter code from television</string>
|
||||
<string name="feat_foryou_connect_subtitle">Make sure to connect to the same Wi-Fi</string>
|
||||
</resources>
|
43
ui/src/main/java/com/m3u/ui/Brushes.kt
Normal file
43
ui/src/main/java/com/m3u/ui/Brushes.kt
Normal file
@ -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)
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user