Milan Jurkulak 3 anni fa
parent
commit
f60a0fa459
34 ha cambiato i file con 328 aggiunte e 106 eliminazioni
  1. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/api/MovieAPI.kt
  2. 4 0
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/StringExt.kt
  3. 13 0
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithBackground.kt
  4. 13 0
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithDescription.kt
  5. 15 0
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithId.kt
  6. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithImage.kt
  7. 13 0
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithMessage.kt
  8. 13 0
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithSubtitle.kt
  9. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithTitle.kt
  10. 6 1
      app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithVideoUri.kt
  11. 30 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/button/Button.kt
  12. 87 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/Card.kt
  13. 10 6
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/CarouselCard.kt
  14. 7 8
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/ItemCard.kt
  15. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/BigCarousel.kt
  16. 17 20
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableCard.kt
  17. 0 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScrollableTvLazyRow.kt
  18. 1 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/TouchBox.kt
  19. 1 3
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt
  20. 0 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Page.kt
  21. 10 3
      app/src/main/kotlin/org/mjdev/tvapp/data/Category.kt
  22. 11 4
      app/src/main/kotlin/org/mjdev/tvapp/data/Message.kt
  23. 29 19
      app/src/main/kotlin/org/mjdev/tvapp/data/Movie.kt
  24. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/mock/MovieList.kt
  25. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/repository/MovieRepository.kt
  26. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/Details.kt
  27. 8 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/Loading.kt
  28. 2 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/MediaPlayer.kt
  29. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/AboutPage.kt
  30. 17 14
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  31. 4 4
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/PluginsPage.kt
  32. 4 4
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/SubscriptionPage.kt
  33. 2 0
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt
  34. 1 0
      app/src/main/res/values/strings.xml

+ 2 - 2
app/src/main/kotlin/org/mjdev/tvapp/api/MovieAPI.kt

@@ -20,7 +20,7 @@ class MovieAPI {
     suspend fun loadCategoryList(): Result<List<Category>> {
         return Result.Success(MovieList.categories.map { name ->
             Category(
-                name = name,
+                title  = name,
                 movieList = getMovieListByCategory(name).unwrapOr(emptyList())
             )
         })
@@ -31,7 +31,7 @@ class MovieAPI {
     }
 
     suspend fun getMovieListByCategory(
-        category: String
+        category: String?
     ): Result<List<Movie>> {
         return Result.Success(MovieList.getByCategory(category))
     }

+ 4 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/StringExt.kt

@@ -8,8 +8,12 @@
 
 package org.mjdev.tvapp.base.extensions
 
+import android.net.Uri
+
 object StringExt {
 
     fun String.asException() = Exception(this)
 
+    fun String.parseUri() = Uri.parse(this)
+
 }

+ 13 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithBackground.kt

@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.interfaces
+
+interface ItemWithBackground {
+    var backgroundImageUrl: Any?
+}

+ 13 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithDescription.kt

@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.interfaces
+
+interface ItemWithDescription {
+    var description: Any?
+}

+ 15 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithId.kt

@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.interfaces
+
+interface ItemWithId {
+    var id: Long
+
+    val hasId: Boolean get() = (id != -1L)
+}

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithImage.kt

@@ -9,5 +9,5 @@
 package org.mjdev.tvapp.base.interfaces
 
 interface ItemWithImage {
-    val backgroundImageUrl : String
+    var imageUrl : Any?
 }

+ 13 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithMessage.kt

@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.interfaces
+
+interface ItemWithMessage {
+    var message: Any?
+}

+ 13 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithSubtitle.kt

@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.interfaces
+
+interface ItemWithSubtitle {
+    var subtitle: Any?
+}

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithTitle.kt

@@ -9,5 +9,5 @@
 package org.mjdev.tvapp.base.interfaces
 
 interface ItemWithTitle {
-    val title : String
+    var title : Any?
 }

+ 6 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/interfaces/ItemWithVideoUri.kt

@@ -12,6 +12,11 @@ import android.net.Uri
 
 interface ItemWithVideoUri {
 
-    val videoUri : Uri
+    var videoUri: Any?
+
+    val hasVideoUri
+        get() = (videoUri != null) &&
+            (videoUri != Uri.EMPTY) &&
+            (videoUri.toString().isNotEmpty())
 
 }

+ 30 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/button/Button.kt

@@ -8,6 +8,7 @@
 
 package org.mjdev.tvapp.base.ui.components.button
 
+import android.annotation.SuppressLint
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.runtime.Composable
@@ -25,13 +26,18 @@ import androidx.tv.material3.ButtonDefaults
 import androidx.tv.material3.ButtonGlow
 import androidx.tv.material3.ButtonScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Glow
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
+import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 @Preview
 @Suppress("UNUSED_PARAMETER")
 @OptIn(ExperimentalTvMaterial3Api::class)
+@SuppressLint("ModifierParameter")
 @Composable
 fun Button(
+    text: Any? = null,
+    onClick: () -> Unit = {},
     modifier: Modifier = Modifier,
     fontSize: TextUnit = 24.sp,
     fontWeight: FontWeight = FontWeight.Bold,
@@ -39,10 +45,11 @@ fun Button(
     containerColor: Color = Color.Transparent,
     contentPadding: PaddingValues = PaddingValues(0.dp),
     shape: Shape = RectangleShape,
-    glow: ButtonGlow = ButtonDefaults.glow(),
-    scale: ButtonScale = ButtonDefaults.scale(),
-    onClick: () -> Unit = {},
-    content: @Composable RowScope.() -> Unit = {}
+    glow: ButtonGlow = ButtonDefaults.NO_GLOW,
+    scale: ButtonScale = ButtonDefaults.NO_SCALE,
+    content: @Composable RowScope.() -> Unit = {
+        TextAny(text = text)
+    }
 ) {
 
     Button(
@@ -56,4 +63,22 @@ fun Button(
         content = content
     )
 
-}
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+val ButtonDefaults.NO_GLOW
+    get() = glow(
+        glow = Glow.None,
+        focusedGlow = Glow.None,
+        pressedGlow = Glow.None
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+val ButtonDefaults.NO_SCALE
+    get() = scale(
+        scale = 1f,
+        focusedScale = 1f,
+        pressedScale = 1f,
+        disabledScale = 1f,
+        focusedDisabledScale = 1f
+    )

+ 87 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/Card.kt

@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.ui.components.card
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.Border
+import androidx.tv.material3.CardBorder
+import androidx.tv.material3.CardColors
+import androidx.tv.material3.CardDefaults
+import androidx.tv.material3.CardGlow
+import androidx.tv.material3.CardScale
+import androidx.tv.material3.CardShape
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.Glow
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Preview
+@Composable
+fun Card(
+    modifier: Modifier = Modifier,
+    scale: CardScale = CardScale.None,
+    onClick: () -> Unit = {},
+    onLongClick: (() -> Unit)? = null,
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    border: CardBorder = CardDefaults.NO_BORDER,
+    glow: CardGlow = CardDefaults.NO_GLOW,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    content: @Composable ColumnScope.() -> Unit = {}
+) = androidx.tv.material3.Card(
+    onClick = onClick,
+    modifier = modifier,
+    onLongClick = onLongClick,
+    shape = shape,
+    colors = colors,
+    scale = scale,
+    border = border,
+    glow = glow,
+    interactionSource = interactionSource,
+    content = content
+)
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+val CardDefaults.NO_BORDER: CardBorder
+    @Composable
+    get() = border(
+        border = Border.None,
+        focusedBorder = Border.None,
+        pressedBorder = Border.None
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+val CardDefaults.NO_GLOW: CardGlow
+    @Composable
+    get() = glow(
+        glow = Glow.None,
+        focusedGlow = Glow.None,
+        pressedGlow = Glow.None
+    )
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun CardDefaults.colorFocusGlow(
+    color: Color = Color.Gray,
+    elevation: Dp = 10.dp
+): CardGlow = glow(
+    glow = Glow.None,
+    focusedGlow = Glow(
+        elevationColor = color,
+        elevation = elevation
+    ),
+    pressedGlow = Glow.None
+)

+ 10 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/CarouselCard.kt

@@ -22,16 +22,19 @@ import androidx.tv.material3.CompactCard
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Glow
 import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.interfaces.ItemWithBackground
+import org.mjdev.tvapp.base.interfaces.ItemWithDescription
+import org.mjdev.tvapp.base.interfaces.ItemWithSubtitle
+import org.mjdev.tvapp.base.interfaces.ItemWithTitle
 import org.mjdev.tvapp.base.ui.components.image.ImageAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
-import org.mjdev.tvapp.data.Movie
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Preview
 @Composable
 @SuppressLint("ModifierParameter")
 fun CarouselCard(
-    movie: Movie? = null,
+    item: Any? = null,
     placeHolder: Int = R.drawable.placeholder,
     modifier: Modifier = Modifier,
     contentScale: ContentScale = ContentScale.Crop,
@@ -45,8 +48,8 @@ fun CarouselCard(
         image = {
             ImageAny(
                 modifier = modifier,
-                src = movie?.cardImageUrl,
-                contentDescription = movie?.description,
+                src = (item as? ItemWithBackground)?.backgroundImageUrl,
+                contentDescription = (item as? ItemWithDescription)?.description?.toString(),
                 contentScale = contentScale,
                 placeholder = placeHolder,
             )
@@ -54,16 +57,17 @@ fun CarouselCard(
         title = {
             TextAny(
                 modifier = Modifier.padding(4.dp),
-                text = movie?.title
+                text = (item as? ItemWithTitle)?.title
             )
         },
         subtitle = {
             TextAny(
                 modifier = Modifier.padding(4.dp),
-                text = movie?.studio
+                text = (item as? ItemWithSubtitle)?.subtitle
             )
         },
         description = {
+          // todo
         },
         glow = CardDefaults.glow(
             glow = Glow.None,

+ 7 - 8
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/MovieCard.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/ItemCard.kt

@@ -16,30 +16,29 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import org.mjdev.tvapp.data.Movie
 import org.mjdev.tvapp.base.ui.components.complex.FocusableCard
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Preview
 @Composable
 @SuppressLint("ModifierParameter")
-fun MovieCard(
-    movie: Movie? = null,
+fun ItemCard(
+    item: Any? = null,
     modifier: Modifier = Modifier,
-    onFocus: (movie: Movie?) -> Unit = {},
-    onClick: (movie: Movie?) -> Unit = {},
+    onFocus: (item: Any?) -> Unit = {},
+    onClick: (item: Any?) -> Unit = {},
 ) {
 
     FocusableCard(
         modifier = modifier
             .widthIn(max = 320.dp)
             .aspectRatio(16f / 9f),
-        movie = movie,
+        item = item,
         onFocus = {
-            onFocus(movie)
+            onFocus(item)
         },
         onClick = {
-            onClick(movie)
+            onClick(item)
         }
     )
 

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/BigCarousel.kt

@@ -76,7 +76,7 @@ fun BigCarousel(
                 .height(height),
             contentScale = ContentScale.Crop,
             scale = CardScale.None,
-            movie = items[indexOfCarouselItem],
+            item = items[indexOfCarouselItem],
             onClick = {
                 onItemClicked(selectedMovie())
             }

+ 17 - 20
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableCard.kt

@@ -29,19 +29,22 @@ import androidx.tv.material3.CardScale
 import androidx.tv.material3.CardShape
 import androidx.tv.material3.CompactCard
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Glow
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
+import org.mjdev.tvapp.base.interfaces.ItemWithDescription
+import org.mjdev.tvapp.base.interfaces.ItemWithImage
+import org.mjdev.tvapp.base.interfaces.ItemWithSubtitle
+import org.mjdev.tvapp.base.interfaces.ItemWithTitle
+import org.mjdev.tvapp.base.ui.components.card.colorFocusGlow
 import org.mjdev.tvapp.base.ui.components.image.ImageAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
-import org.mjdev.tvapp.data.Movie
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @SuppressLint("ModifierParameter")
 @Preview
 @Composable
 fun FocusableCard(
-    movie: Movie? = null,
+    item: Any? = null,
     modifier: Modifier = Modifier,
     contentScale: ContentScale = ContentScale.Crop,
     placeHolder: Int = R.drawable.placeholder,
@@ -51,8 +54,8 @@ fun FocusableCard(
     border: CardBorder = CardDefaults.border(),
     glow: CardGlow = CardDefaults.glow(),
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    onFocus: (movie: Movie?) -> Unit = {},
-    onClick: (movie: Movie?) -> Unit = {},
+    onFocus: (item: Any?) -> Unit = {},
+    onClick: (item: Any?) -> Unit = {},
 ) {
     val focusState = remember { mutableStateOf<FocusState?>(null) }
     val isFocused: () -> Boolean = {
@@ -65,29 +68,22 @@ fun FocusableCard(
         shape = shape,
         colors = colors,
         border = border,
-        glow = CardDefaults.glow(
-            glow = Glow.None,
-            focusedGlow = Glow(
-                elevationColor = Color.Green,
-                elevation = 10.dp
-            ),
-            pressedGlow = Glow.None
-        ),
+        glow = CardDefaults.colorFocusGlow(Color.Green),
         modifier = modifier
             .onFocusChanged { state ->
                 focusState.value = state
             }
             .touchable {
                 if (isFocused())
-                    onClick(movie)
+                    onClick(item)
                 else
-                    onFocus(movie)
+                    onFocus(item)
             },
         image = {
             ImageAny(
                 modifier = modifier,
-                src = movie?.cardImageUrl,
-                contentDescription = movie?.description,
+                src = (item as? ItemWithImage)?.imageUrl,
+                contentDescription = (item as? ItemWithDescription)?.description?.toString(),
                 contentScale = contentScale,
                 placeholder = placeHolder
             )
@@ -95,19 +91,20 @@ fun FocusableCard(
         title = {
             TextAny(
                 modifier = Modifier.padding(4.dp),
-                text = movie?.title
+                text = (item as? ItemWithTitle)?.title
             )
         },
         subtitle = {
             TextAny(
                 modifier = Modifier.padding(4.dp),
-                text = movie?.studio
+                text = (item as? ItemWithSubtitle)?.subtitle
             )
         },
         description = {
+          // todo
         },
         onClick = {
-            onClick(movie)
+            onClick(item)
         },
     )
 

+ 0 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScrollableTvLayRow.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScrollableTvLazyRow.kt


+ 1 - 2
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/TouchBox.kt

@@ -32,6 +32,7 @@ fun TouchBox(
     state: TvLazyListState? = null,
     content: @Composable BoxScope.() -> Unit = {}
 ) {
+
     val scrollDelta = remember { mutableFloatStateOf(0f) }
 
     Box(
@@ -43,9 +44,7 @@ fun TouchBox(
         contentAlignment = contentAlignment,
         propagateMinConstraints = propagateMinConstraints,
     ) {
-
         content.invoke(this)
-
     }
 
     LaunchedEffect(scrollDelta.value) {

+ 1 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt

@@ -11,7 +11,6 @@ package org.mjdev.tvapp.base.ui.components.navigation
 import android.annotation.SuppressLint
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
@@ -38,7 +37,7 @@ fun Navigation(
     navigationState: NavigationState = rememberNavigationState(
         navController = navController,
         drawerState = rememberDrawerState(
-            initialValue = DrawerValue.Open
+            initialValue = DrawerValue.Closed
         )
     ),
     backgroundColor: Color = Color(0xff202020),
@@ -53,7 +52,6 @@ fun Navigation(
     val isEdit = isEditMode()
 
     if (isEdit) navigationState.openDrawer()
-    else navigationState.closeDrawer()
 
     navController.addMenuItem(*menuItems.toTypedArray())
 

+ 0 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Page.kt

@@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextDirection.Companion.Content
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp

+ 10 - 3
app/src/main/kotlin/org/mjdev/tvapp/data/Category.kt

@@ -9,9 +9,16 @@
 package org.mjdev.tvapp.data
 
 import androidx.compose.runtime.Stable
+import org.mjdev.tvapp.base.interfaces.ItemWithId
+import org.mjdev.tvapp.base.interfaces.ItemWithTitle
+import java.io.Serializable
 
 @Stable
 data class Category(
-    var name: String,
-    var movieList: List<Movie>
-)
+
+    override var id: Long = 0L,
+    override var title: Any? = "",
+
+    var movieList: List<Movie> = emptyList(),
+
+) : Serializable, ItemWithTitle, ItemWithId

+ 11 - 4
app/src/main/kotlin/org/mjdev/tvapp/data/Message.kt

@@ -8,10 +8,17 @@
 
 package org.mjdev.tvapp.data
 
+import org.mjdev.tvapp.base.interfaces.ItemWithId
+import org.mjdev.tvapp.base.interfaces.ItemWithMessage
+import org.mjdev.tvapp.base.interfaces.ItemWithTitle
+
 data class Message(
-    val id: Int = 0,
-    val title: String = "",
-    val message: String,
+
+    override var id: Long = 0,
+    override var title: Any? = "",
+    override var message: Any? = "",
+
     val read: Boolean = false,
     val deleted: Boolean = false
-)
+
+) : ItemWithTitle, ItemWithMessage, ItemWithId

+ 29 - 19
app/src/main/kotlin/org/mjdev/tvapp/data/Movie.kt

@@ -9,37 +9,47 @@
 package org.mjdev.tvapp.data
 
 import android.net.Uri
+import org.mjdev.tvapp.base.interfaces.ItemWithBackground
+import org.mjdev.tvapp.base.interfaces.ItemWithDescription
+import org.mjdev.tvapp.base.interfaces.ItemWithId
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
+import org.mjdev.tvapp.base.interfaces.ItemWithSubtitle
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
 import org.mjdev.tvapp.base.interfaces.ItemWithVideoUri
 import java.io.Serializable
 
 data class Movie(
 
-    var id: Long = 0,
-    override var title: String = "",
-    var description: String = "",
-    override var backgroundImageUrl: String = "",
-    var cardImageUrl: String = "",
-    override var videoUri: Uri = Uri.EMPTY,
-    var studio: String = ""
+    var studio: Any? = "",
 
-) : Serializable, ItemWithTitle, ItemWithImage, ItemWithVideoUri {
+    override var id: Long = 0,
+    override var title: Any? = "",
+    override var description: Any? = "",
+    override var backgroundImageUrl: Any? = "",
+    override var imageUrl: Any? = "",
+    override var videoUri: Any? = Uri.EMPTY,
 
-    val hasVideoUri get() = videoUri != Uri.EMPTY
+    ) : Serializable,
+    ItemWithId,
+    ItemWithTitle,
+    ItemWithSubtitle,
+    ItemWithImage,
+    ItemWithVideoUri,
+    ItemWithBackground,
+    ItemWithDescription {
+
+    override var subtitle: Any?
+        get() = studio
+        set(value) { studio = value }
 
     override fun toString(): String {
         return "Movie{" +
-                "id=" + id +
-                ", title='" + title + '\'' +
-                ", videoUrl='" + videoUri.toString() + '\'' +
-                ", backgroundImageUrl='" + backgroundImageUrl + '\'' +
-                ", cardImageUrl='" + cardImageUrl + '\'' +
-                '}'
-    }
-
-    companion object {
-        internal const val serialVersionUID = 727566175075960653L
+            "id=" + id +
+            ", title='" + title + '\'' +
+            ", videoUrl='" + videoUri.toString() + '\'' +
+            ", backgroundImageUrl='" + backgroundImageUrl + '\'' +
+            ", cardImageUrl='" + imageUrl + '\'' +
+            '}'
     }
 
 }

+ 2 - 2
app/src/main/kotlin/org/mjdev/tvapp/mock/MovieList.kt

@@ -35,7 +35,7 @@ internal object MovieList {
         categories.associateWith { all.shuffled() }
     }
 
-    fun getByCategory(category: String): List<Movie> {
+    fun getByCategory(category: String?): List<Movie> {
         return categoryMoviesMap[category] ?: all
     }
 
@@ -96,7 +96,7 @@ internal object MovieList {
                 description = description,
                 studio = studio[it],
                 videoUri = Uri.parse(videoUrl[it]),
-                cardImageUrl = cardImageUrl[it],
+                imageUrl = cardImageUrl[it],
                 backgroundImageUrl = bgImageUrl[it],
             )
         }

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/repository/MovieRepository.kt

@@ -76,7 +76,7 @@ class MovieRepository @Inject constructor(
         if (force || !categoryMovieListMap.contains(category)) {
             updateCache(
                 category = category,
-                movieList = dataSource.getMovieListByCategory(category.name).unwrapOr(listOf())
+                movieList = dataSource.getMovieListByCategory(category.title?.toString()).unwrapOr(listOf())
             )
         }
     }

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/components/Details.kt

@@ -42,7 +42,7 @@ fun Details(
     ) {
 
         ImageAny(
-            src = movie?.cardImageUrl,
+            src = movie?.imageUrl,
             contentDescription = null,
             contentScale = ContentScale.Crop,
             modifier = Modifier.fillMaxSize()

+ 8 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/components/Loading.kt

@@ -8,12 +8,15 @@
 
 package org.mjdev.tvapp.ui.components
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.unit.sp
 import androidx.tv.material3.MaterialTheme
 import androidx.tv.material3.Text
 import org.mjdev.tvapp.base.annotations.TvPreview
@@ -23,12 +26,16 @@ import org.mjdev.tvapp.base.annotations.TvPreview
 fun Loading(modifier: Modifier = Modifier) {
 
     Box(
-        modifier = modifier.fillMaxSize(),
+        modifier = modifier.fillMaxSize().background(
+            Color.DarkGray,
+            RectangleShape
+        ),
         contentAlignment = Alignment.Center
     ) {
         Text(
             text = "Loading...",
             color = Color.White,
+            fontSize=20.sp,
             style = MaterialTheme.typography.headlineMedium
         )
     }

+ 2 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/components/MediaPlayer.kt

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.extensions.StringExt.parseUri
 import org.mjdev.tvapp.base.interfaces.ItemWithVideoUri
 import org.mjdev.tvapp.base.ui.components.media.MediaPlayerContainer
 
@@ -26,7 +27,7 @@ fun MediaPlayer(
 
     MediaPlayerContainer(
         modifier = modifier.fillMaxSize(),
-        uri = movie?.videoUri
+        uri = movie?.videoUri.toString().parseUri()
     )
 
 }

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/AboutPage.kt

@@ -24,11 +24,11 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.Card
 import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.card.Card
 import org.mjdev.tvapp.base.ui.components.page.Page
 import org.mjdev.tvapp.base.ui.components.card.CardContent
 import org.mjdev.tvapp.base.ui.components.image.CircleImage

+ 17 - 14
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Home
 import androidx.compose.runtime.Composable
-
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -31,23 +30,24 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyRow
 import androidx.tv.foundation.lazy.list.items
-import androidx.tv.material3.Text
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
 import org.mjdev.tvapp.base.extensions.HiltExt.appViewModel
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
 import org.mjdev.tvapp.base.extensions.StringExt.asException
+import org.mjdev.tvapp.base.interfaces.ItemWithId
+import org.mjdev.tvapp.base.interfaces.ItemWithVideoUri
 import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.open
 import org.mjdev.tvapp.base.network.NetworkStatus
 import org.mjdev.tvapp.base.ui.components.page.Page
 import org.mjdev.tvapp.base.ui.components.complex.BigCarousel
 import org.mjdev.tvapp.base.ui.components.complex.ScrollableTvLazyRow
 import org.mjdev.tvapp.base.ui.components.complex.Tabs
-import org.mjdev.tvapp.data.Movie
-import org.mjdev.tvapp.base.ui.components.card.MovieCard
+import org.mjdev.tvapp.base.ui.components.card.ItemCard
 import org.mjdev.tvapp.base.ui.components.complex.ErrorMessage
 import org.mjdev.tvapp.base.ui.components.complex.Header
+import org.mjdev.tvapp.base.ui.components.text.TextAny
 import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.PlayerScreen
 import org.mjdev.tvapp.viewmodel.MainViewModel
@@ -74,13 +74,16 @@ class MainPage : Page() {
         val errorState = remember { mutableStateOf<Throwable?>(null) }
         val titleState = remember { mutableStateOf<Any?>(R.string.app_name) }
 
-        val onItemClick: (movie: Movie?) -> Unit = { movie ->
-            if (movie == null) {
+        val onItemClick: (item: Any?) -> Unit = { item ->
+            val isVideo = ((item as? ItemWithVideoUri)?.hasVideoUri == true)
+            val isId = ((item as? ItemWithId)?.hasId == true)
+            val id = (item as? ItemWithId)?.id
+            if ((item == null) || (!isId)) {
                 viewModel.postError("No media found.".asException())
-            } else if (movie.hasVideoUri) {
-                navController?.open<PlayerScreen>(movie.id)
+            } else if (isVideo) {
+                navController?.open<PlayerScreen>(id)
             } else {
-                navController?.open<DetailScreen>(movie.id)
+                navController?.open<DetailScreen>(id)
             }
         }
 
@@ -116,7 +119,7 @@ class MainPage : Page() {
             }
             if (isEdit || categoryList.value.isNotEmpty()) item {
                 Tabs(
-                    items = categoryList.value.map { it.name }
+                    items = categoryList.value.map { category -> category.title }
                 )
             }
             if (isEdit || featuredMovieList.value.isNotEmpty()) item {
@@ -127,11 +130,11 @@ class MainPage : Page() {
                 )
             }
             items(categoryList.value) { category ->
-                Text(
+                TextAny(
                     modifier = Modifier
                         .fillMaxWidth()
                         .padding(0.dp, 16.dp),
-                    text = category.name,
+                    text = category.title,
                     color = Color.White,
                     fontWeight = FontWeight.Bold
                 )
@@ -146,8 +149,8 @@ class MainPage : Page() {
                     )
                 ) {
                     items(category.movieList) { item ->
-                        MovieCard(
-                            movie = item,
+                        ItemCard(
+                            item = item,
                             onClick = onItemClick
                         )
                     }

+ 4 - 4
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/PluginsPage.kt

@@ -14,24 +14,24 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.SettingsInputComponent
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.Card
 import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.card.Card
 import org.mjdev.tvapp.base.ui.components.page.Page
 
 class PluginsPage : Page() {
 
-    override val title: Int = R.string.title_about
-    override val icon: ImageVector = Icons.Default.Info
+    override val title: Int = R.string.title_plugins
+    override val icon: ImageVector = Icons.Default.SettingsInputComponent
 
     @OptIn(ExperimentalTvMaterial3Api::class)
     @TvPreview

+ 4 - 4
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/SubscriptionPage.kt

@@ -22,24 +22,24 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
-import androidx.tv.material3.Card
 import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.card.Card
 import org.mjdev.tvapp.base.ui.components.page.Page
 import org.mjdev.tvapp.base.ui.components.card.CardContent
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 class SubscriptionPage : Page() {
 
-    override val title : Int = R.string.title_subscription
-    override val icon : ImageVector = Icons.Default.ShoppingCart
+    override val title: Int = R.string.title_subscription
+    override val icon: ImageVector = Icons.Default.ShoppingCart
 
     @OptIn(ExperimentalTvMaterial3Api::class)
     @TvPreview
     @Composable
-    override fun Content () {
+    override fun Content() {
         Box(
             modifier = Modifier
                 .fillMaxSize()

+ 2 - 0
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt

@@ -20,6 +20,7 @@ import org.mjdev.tvapp.base.ui.components.page.SearchPage
 import org.mjdev.tvapp.base.ui.components.page.SettingsPage
 import org.mjdev.tvapp.ui.pages.AboutPage
 import org.mjdev.tvapp.ui.pages.MainPage
+import org.mjdev.tvapp.ui.pages.PluginsPage
 import org.mjdev.tvapp.ui.pages.SubscriptionPage
 
 class MainScreen : ScreenWithPages() {
@@ -41,6 +42,7 @@ class MainScreen : ScreenWithPages() {
         page(MainPage())
         page(SubscriptionPage())
 
+        page(PluginsPage())
         page(SettingsPage())
 
     }

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -33,5 +33,6 @@
     <string name="title_subscription">Subscription</string>
 
     <string name="author">Created by Milan Jurkulak © 2023</string>
+    <string name="title_plugins">Plugins</string>
 
 </resources>