11package com.m3u.tv.screens.foryou
22
33import androidx.compose.foundation.BorderStroke
4+ import androidx.compose.foundation.background
45import androidx.compose.foundation.focusGroup
6+ import androidx.compose.foundation.layout.Arrangement
57import androidx.compose.foundation.layout.Box
8+ import androidx.compose.foundation.layout.Column
69import androidx.compose.foundation.layout.PaddingValues
10+ import androidx.compose.foundation.layout.Spacer
711import androidx.compose.foundation.layout.fillMaxSize
812import androidx.compose.foundation.layout.fillMaxWidth
913import androidx.compose.foundation.layout.height
1014import androidx.compose.foundation.layout.heightIn
1115import androidx.compose.foundation.layout.padding
16+ import androidx.compose.foundation.layout.size
1217import androidx.compose.foundation.layout.width
1318import androidx.compose.foundation.lazy.LazyColumn
1419import androidx.compose.foundation.lazy.LazyRow
1520import androidx.compose.foundation.lazy.rememberLazyListState
21+ import androidx.compose.material.icons.Icons
22+ import androidx.compose.material.icons.rounded.LiveTv
23+ import androidx.compose.material.icons.rounded.Movie
24+ import androidx.compose.material.icons.rounded.Tv
1625import androidx.compose.runtime.Composable
1726import androidx.compose.runtime.LaunchedEffect
1827import androidx.compose.runtime.derivedStateOf
1928import androidx.compose.runtime.getValue
2029import androidx.compose.runtime.remember
30+ import androidx.compose.ui.Alignment
2131import androidx.compose.ui.Modifier
2232import androidx.compose.ui.graphics.Color
2333import androidx.compose.ui.text.font.FontWeight
@@ -27,15 +37,22 @@ import androidx.compose.ui.unit.sp
2737import androidx.hilt.navigation.compose.hiltViewModel
2838import androidx.lifecycle.compose.collectAsStateWithLifecycle
2939import androidx.tv.material3.Border
40+ import androidx.tv.material3.Card
3041import androidx.tv.material3.CardDefaults
3142import androidx.tv.material3.CompactCard
43+ import androidx.tv.material3.ExperimentalTvMaterial3Api
44+ import androidx.tv.material3.Icon
3245import androidx.tv.material3.MaterialTheme
3346import androidx.tv.material3.Text
3447import com.m3u.business.foryou.ForyouViewModel
3548import com.m3u.business.foryou.Recommend
49+ import com.m3u.core.architecture.preferences.PreferencesKeys
50+ import com.m3u.core.architecture.preferences.preferenceOf
3651import com.m3u.core.foundation.components.AbsoluteSmoothCornerShape
3752import com.m3u.core.foundation.ui.SugarColors
53+ import com.m3u.data.database.model.DataSource
3854import com.m3u.data.database.model.Playlist
55+ import com.m3u.data.database.model.type
3956import com.m3u.tv.screens.dashboard.rememberChildPadding
4057import com.m3u.tv.theme.LexendExa
4158
@@ -72,7 +89,7 @@ private fun Catalog(
7289 modifier : Modifier = Modifier ,
7390 isTopBarVisible : Boolean = true,
7491) {
75-
92+ val contentTypeMode by preferenceOf( PreferencesKeys . CONTENT_TYPE_MODE )
7693 val lazyListState = rememberLazyListState()
7794 val childPadding = rememberChildPadding()
7895
@@ -123,65 +140,228 @@ private fun Catalog(
123140 }
124141
125142 item(contentType = " PlaylistsRow" ) {
126- val startPadding: Dp = rememberChildPadding().start
127- val endPadding: Dp = rememberChildPadding().end
128- val shape = AbsoluteSmoothCornerShape (16 .dp, 100 )
129- LazyRow (
130- modifier = Modifier
131- .focusGroup()
132- .padding(top = 16 .dp),
133- contentPadding = PaddingValues (start = startPadding, end = endPadding)
134- ) {
135- val entries = playlists.entries.toList()
136- items(entries.size) {
137- val (playlist, _) = entries[it]
138- val (color, contentColor) = remember {
139- SugarColors .entries.random()
140- }
141- CompactCard (
142- onClick = { navigateToPlaylist(playlist.url) },
143- title = {
144- Text (
145- text = playlist.title,
146- modifier = Modifier .padding(16 .dp),
147- fontSize = 36 .sp,
148- lineHeight = 36 .sp,
149- fontWeight = FontWeight .Bold ,
150- fontFamily = LexendExa
151- )
152- },
153- colors = CardDefaults .compactCardColors(
154- containerColor = color,
155- contentColor = MaterialTheme .colorScheme.background
156- ),
157- shape = CardDefaults .shape(shape),
158- border = CardDefaults .border(
159- border = Border (
160- BorderStroke (
161- width = 2 .dp,
162- color = MaterialTheme .colorScheme.border
163- ),
164- shape = shape
165- ),
166- focusedBorder = Border (
167- BorderStroke (width = 4 .dp, color = Color .White ),
168- shape = shape
143+ if (contentTypeMode) {
144+ // Show Content Type Cards (Live TV, Movies, Series)
145+ // Filter to only show Xtream playlists with specific types
146+ val xtreamPlaylists = playlists.keys.filter {
147+ it.source == DataSource .Xtream && it.type != null
148+ }
149+
150+ ContentTypeCardsRow (
151+ playlists = xtreamPlaylists.associateWith { playlists[it] ? : 0 },
152+ navigateToPlaylist = navigateToPlaylist,
153+ modifier = Modifier .padding(top = 16 .dp)
154+ )
155+ } else {
156+ // Show Traditional Playlist Cards
157+ val startPadding: Dp = rememberChildPadding().start
158+ val endPadding: Dp = rememberChildPadding().end
159+ val shape = AbsoluteSmoothCornerShape (16 .dp, 100 )
160+ LazyRow (
161+ modifier = Modifier
162+ .focusGroup()
163+ .padding(top = 16 .dp),
164+ contentPadding = PaddingValues (start = startPadding, end = endPadding)
165+ ) {
166+ val entries = playlists.entries.toList()
167+ items(entries.size) {
168+ val (playlist, _) = entries[it]
169+ val (color, contentColor) = remember {
170+ SugarColors .entries.random()
171+ }
172+ CompactCard (
173+ onClick = { navigateToPlaylist(playlist.url) },
174+ title = {
175+ Text (
176+ text = playlist.title,
177+ modifier = Modifier .padding(16 .dp),
178+ fontSize = 36 .sp,
179+ lineHeight = 36 .sp,
180+ fontWeight = FontWeight .Bold ,
181+ fontFamily = LexendExa
182+ )
183+ },
184+ colors = CardDefaults .compactCardColors(
185+ containerColor = color,
186+ contentColor = MaterialTheme .colorScheme.background
169187 ),
170- pressedBorder = Border (
171- BorderStroke (
172- width = 4 .dp,
173- color = MaterialTheme .colorScheme.border
188+ shape = CardDefaults .shape(shape),
189+ border = CardDefaults .border(
190+ border = Border (
191+ BorderStroke (
192+ width = 2 .dp,
193+ color = MaterialTheme .colorScheme.border
194+ ),
195+ shape = shape
196+ ),
197+ focusedBorder = Border (
198+ BorderStroke (width = 4 .dp, color = Color .White ),
199+ shape = shape
174200 ),
175- shape = shape
176- )
177- ),
178- image = {},
179- modifier = Modifier
180- .width(265 .dp)
181- .heightIn(min = 130 .dp)
182- )
201+ pressedBorder = Border (
202+ BorderStroke (
203+ width = 4 .dp,
204+ color = MaterialTheme .colorScheme.border
205+ ),
206+ shape = shape
207+ )
208+ ),
209+ image = {},
210+ modifier = Modifier
211+ .width(265 .dp)
212+ .heightIn(min = 130 .dp)
213+ )
214+ }
183215 }
184216 }
185217 }
186218 }
187219}
220+
221+ @OptIn(ExperimentalTvMaterial3Api ::class )
222+ @Composable
223+ private fun ContentTypeCardsRow (
224+ playlists : Map <Playlist , Int >,
225+ navigateToPlaylist : (playlistUrl: String ) -> Unit ,
226+ modifier : Modifier = Modifier
227+ ) {
228+ val startPadding: Dp = rememberChildPadding().start
229+ val endPadding: Dp = rememberChildPadding().end
230+
231+ // Find type-specific Xtream playlists
232+ val livePlaylist = playlists.keys.firstOrNull {
233+ it.source == DataSource .Xtream && it.type == DataSource .Xtream .TYPE_LIVE
234+ }
235+ val vodPlaylist = playlists.keys.firstOrNull {
236+ it.source == DataSource .Xtream && it.type == DataSource .Xtream .TYPE_VOD
237+ }
238+ val seriesPlaylist = playlists.keys.firstOrNull {
239+ it.source == DataSource .Xtream && it.type == DataSource .Xtream .TYPE_SERIES
240+ }
241+
242+ if (livePlaylist == null && vodPlaylist == null && seriesPlaylist == null ) {
243+ // Show warning if no type-specific Xtream playlists found
244+ Box (
245+ modifier = modifier
246+ .fillMaxWidth()
247+ .padding(horizontal = startPadding, vertical = 16 .dp)
248+ .background(
249+ MaterialTheme .colorScheme.errorContainer.copy(alpha = 0.3f ),
250+ MaterialTheme .shapes.medium
251+ )
252+ .padding(24 .dp),
253+ contentAlignment = Alignment .Center
254+ ) {
255+ Text (
256+ text = " ⚠️ Content Type Mode requires Xtream Codes playlist.\n Please add an Xtream playlist or disable Content Type Mode in Settings." ,
257+ style = MaterialTheme .typography.bodyLarge,
258+ color = MaterialTheme .colorScheme.error,
259+ fontWeight = FontWeight .Medium
260+ )
261+ }
262+ return
263+ }
264+
265+ LazyRow (
266+ modifier = modifier.focusGroup(),
267+ contentPadding = PaddingValues (start = startPadding, end = endPadding),
268+ horizontalArrangement = Arrangement .spacedBy(16 .dp)
269+ ) {
270+ // Live TV Card
271+ if (livePlaylist != null ) {
272+ item {
273+ ContentTypeCard (
274+ title = " Live TV" ,
275+ subtitle = " ${playlists[livePlaylist] ? : 0 } channels" ,
276+ icon = Icons .Rounded .LiveTv ,
277+ containerColor = Color (0xFF10B981 ), // Teal/Green
278+ onClick = { navigateToPlaylist(livePlaylist.url) }
279+ )
280+ }
281+ }
282+
283+ // Movies Card
284+ if (vodPlaylist != null ) {
285+ item {
286+ ContentTypeCard (
287+ title = " Movies" ,
288+ subtitle = " ${playlists[vodPlaylist] ? : 0 } movies" ,
289+ icon = Icons .Rounded .Movie ,
290+ containerColor = Color (0xFFA855F7 ), // Purple
291+ onClick = { navigateToPlaylist(vodPlaylist.url) }
292+ )
293+ }
294+ }
295+
296+ // Series Card
297+ if (seriesPlaylist != null ) {
298+ item {
299+ ContentTypeCard (
300+ title = " Series" ,
301+ subtitle = " ${playlists[seriesPlaylist] ? : 0 } series" ,
302+ icon = Icons .Rounded .Tv ,
303+ containerColor = Color (0xFFF97316 ), // Orange
304+ onClick = { navigateToPlaylist(seriesPlaylist.url) }
305+ )
306+ }
307+ }
308+ }
309+ }
310+
311+ @OptIn(ExperimentalTvMaterial3Api ::class )
312+ @Composable
313+ private fun ContentTypeCard (
314+ title : String ,
315+ subtitle : String ,
316+ icon : androidx.compose.ui.graphics.vector.ImageVector ,
317+ containerColor : Color ,
318+ onClick : () -> Unit ,
319+ modifier : Modifier = Modifier
320+ ) {
321+ Card (
322+ onClick = onClick,
323+ modifier = modifier
324+ .width(320 .dp)
325+ .height(180 .dp),
326+ colors = CardDefaults .colors(
327+ containerColor = containerColor,
328+ contentColor = Color .White
329+ ),
330+ shape = CardDefaults .shape(MaterialTheme .shapes.medium),
331+ border = CardDefaults .border(
332+ focusedBorder = Border (
333+ BorderStroke (4 .dp, Color .White ),
334+ shape = MaterialTheme .shapes.medium
335+ )
336+ )
337+ ) {
338+ Box (
339+ modifier = Modifier .fillMaxSize(),
340+ contentAlignment = Alignment .Center
341+ ) {
342+ Column (
343+ horizontalAlignment = Alignment .CenterHorizontally ,
344+ verticalArrangement = Arrangement .Center
345+ ) {
346+ Icon (
347+ imageVector = icon,
348+ contentDescription = title,
349+ modifier = Modifier .size(64 .dp),
350+ tint = Color .White
351+ )
352+ Spacer (modifier = Modifier .height(12 .dp))
353+ Text (
354+ text = title,
355+ style = MaterialTheme .typography.titleLarge,
356+ fontWeight = FontWeight .Bold ,
357+ color = Color .White
358+ )
359+ Text (
360+ text = subtitle,
361+ style = MaterialTheme .typography.bodyMedium,
362+ color = Color .White .copy(alpha = 0.8f )
363+ )
364+ }
365+ }
366+ }
367+ }
0 commit comments