2
0
Milan Jurkulak 3 жил өмнө
parent
commit
08dc1a485c
35 өөрчлөгдсөн 568 нэмэгдсэн , 460 устгасан
  1. 0 4
      app/src/main/kotlin/org/mjdev/tvapp/base/activity/ComposableActivity.kt
  2. 29 0
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ListExt.kt
  3. 41 5
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/NavExt.kt
  4. 0 71
      app/src/main/kotlin/org/mjdev/tvapp/base/helpers/MockedViewModels.kt
  5. 0 42
      app/src/main/kotlin/org/mjdev/tvapp/base/helpers/StateContextWrapper.kt
  6. 8 0
      app/src/main/kotlin/org/mjdev/tvapp/base/media/PlayerControls.kt
  7. 8 0
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/EmptyScreen.kt
  8. 6 13
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItem.kt
  9. 15 0
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItemClickListener.kt
  10. 1 13
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavGraphBuilderEx.kt
  11. 57 0
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostControllerEx.kt
  12. 2 3
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostEx.kt
  13. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/Screen.kt
  14. 15 6
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/ScreenWithPages.kt
  15. 15 5
      app/src/main/kotlin/org/mjdev/tvapp/base/page/Page.kt
  16. 0 11
      app/src/main/kotlin/org/mjdev/tvapp/base/page/PageState.kt
  17. 45 19
      app/src/main/kotlin/org/mjdev/tvapp/base/page/Pager.kt
  18. 17 14
      app/src/main/kotlin/org/mjdev/tvapp/base/page/PagerScope.kt
  19. 27 27
      app/src/main/kotlin/org/mjdev/tvapp/base/page/PagerState.kt
  20. 51 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/CardContent.kt
  21. 2 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ErrorMessage.kt
  22. 8 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableCard.kt
  23. 0 83
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ListWithPreview.kt
  24. 0 97
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/MyComposeView.kt
  25. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScreenView.kt
  26. 33 8
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt
  27. 76 5
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/AboutPage.kt
  28. 12 5
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  29. 66 8
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/SubscriptionPage.kt
  30. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  31. 4 3
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt
  32. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/PlayerScreen.kt
  33. 14 3
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt
  34. 8 6
      app/src/main/res/values/strings.xml
  35. 1 1
      gradle.properties

+ 0 - 4
app/src/main/kotlin/org/mjdev/tvapp/base/activity/ComposableActivity.kt

@@ -21,7 +21,6 @@ import androidx.compose.ui.graphics.RectangleShape
 import androidx.core.view.WindowCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
-import androidx.navigation.compose.rememberNavController
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.NonInteractiveSurfaceDefaults
 import androidx.tv.material3.Surface
@@ -70,11 +69,8 @@ open class ComposableActivity : ComponentActivity() {
                 )
             ) {
 
-                val navController = rememberNavController()
-
                 NavHostEx(
                     modifier = Modifier.fillMaxSize(),
-                    navController = navController,
                     builder = navGraphBuilder
                 )
 

+ 29 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ListExt.kt

@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.extensions
+
+object ListExt {
+
+    fun <T> MutableList<T>.addUnique(element: T, replace: Boolean = true) {
+        val duplicate = firstOrNull { p ->
+            p.toString().contentEquals(element.toString()) || p?.equals(element) == true
+        }
+        if (duplicate == null) {
+            add(element)
+        } else {
+            if (replace) {
+                remove(duplicate)
+                add(element)
+            } else {
+                throw (RuntimeException("Duplicate item."))
+            }
+        }
+    }
+
+}

+ 41 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/NavExt.kt

@@ -8,16 +8,24 @@
 
 package org.mjdev.tvapp.base.extensions
 
-import androidx.navigation.NavController
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.platform.LocalContext
+import androidx.navigation.NavDestination
 import androidx.navigation.NavGraph
-import androidx.navigation.NavHostController
+import androidx.navigation.Navigator
 import androidx.navigation.NavigatorProvider
+import androidx.navigation.compose.ComposeNavigator
+import androidx.navigation.compose.DialogNavigator
 import org.mjdev.tvapp.base.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 
 object NavExt {
 
-    inline fun NavController.createGraph(
-        navController: NavHostController,
+    inline fun NavHostControllerEx.createGraph(
+        navController: NavHostControllerEx,
         route: String? = null,
         startRoute: String? = null,
         builder: NavGraphBuilderEx.() -> Unit
@@ -25,7 +33,7 @@ object NavExt {
 
 
     inline fun NavigatorProvider.navigation(
-        navController: NavHostController,
+        navController: NavHostControllerEx,
         route: String? = null,
         startRoute: String? = null,
         builder: NavGraphBuilderEx.() -> Unit
@@ -33,4 +41,32 @@ object NavExt {
         .apply(builder)
         .build()
 
+    @Composable
+    fun rememberNavControllerEx(
+        vararg navigators: Navigator<out NavDestination>
+    ): NavHostControllerEx {
+        val context = LocalContext.current
+        return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) {
+            createNavController(context)
+        }.apply {
+            for (navigator in navigators) {
+                navigatorProvider.addNavigator(navigator)
+            }
+        }
+    }
+
+    private fun createNavController(context: Context) =
+        NavHostControllerEx(context).apply {
+            navigatorProvider.addNavigator(ComposeNavigator())
+            navigatorProvider.addNavigator(DialogNavigator())
+        }
+
+    @Suppress("FunctionName")
+    private fun NavControllerSaver(
+        context: Context
+    ): Saver<NavHostControllerEx, *> = Saver(
+        save = { it.saveState() },
+        restore = { createNavController(context).apply { restoreState(it) } }
+    )
+
 }

+ 0 - 71
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/MockedViewModels.kt

@@ -1,71 +0,0 @@
-//package org.mjdev.tvapp.base.helpers
-//
-//import android.content.Context
-//import androidx.lifecycle.ViewModel
-//import androidx.lifecycle.ViewModelProvider
-//import androidx.lifecycle.viewmodel.CreationExtras
-//import kotlin.jvm.internal.Reflection
-//import kotlin.reflect.KClass
-//import kotlin.reflect.KFunction
-//import kotlin.reflect.full.companionObject
-//import kotlin.reflect.full.functions
-//
-//// todo: preview android studio does not support reflection
-//// todo: this works runtime only
-//class MockedViewModels<VM : ViewModel>(
-//    val context: Context
-//) : ViewModelProvider.Factory {
-//
-//    private val KClass<*>.viewModelFncName: String
-//        get() = String.format("mock%s", simpleName)
-//
-//    private val KClass<*>.companion : KClass<*> get() {
-//        return companionObject ?: createCompanion()
-//    }
-//
-//    private fun KClass<*>.createCompanion() : KClass<*> {
-//        val cname = qualifiedName?.let {
-//            String.format("%s.Companion", it)
-//        } ?: throw(RuntimeException("Can not create companion class $this"))
-//        val clazz =  Class.forName(cname)
-//            ?: throw(RuntimeException("Can not create companion class $cname"))
-//        return clazz.kt
-//    }
-//
-//    private val KClass<*>.viewModelFnc: KFunction<*>
-//        get() {
-//            val companion = companion
-//            val fnc = companion.functions.firstOrNull { fn -> fn.name == viewModelFncName }
-//            return fnc ?: throw (RuntimeException("No function $viewModelFncName exists in $this."))
-//        }
-//
-//    fun KFunction<*>.invoke(context: Context): Any? {
-//        val instance = (parameters[0].type.classifier as KClass<*>).objectInstance
-//            ?: throw (RuntimeException("Cant create instance of ${parameters[0].type.classifier}"))
-//        return call(instance, context)
-//    }
-//
-//    private fun KClass<*>.viewModelInstance(
-//        context: Context,
-//    ): Any? {
-//        return viewModelFnc.invoke(context)
-//    }
-//
-//    fun KClass<*>.get(context: Context): Any {
-//        return viewModelInstance(context)
-//            ?: throw (RuntimeException("No model instance"))
-//    }
-//
-//    val <T : Any> Class<T>.kt: KClass<T>
-//        get() = Reflection.getOrCreateKotlinClass(this) as KClass<T>
-//
-//    override fun <VM : ViewModel> create(
-//        modelClass: Class<VM>,
-//        extras: CreationExtras
-//    ): VM = modelClass.kt.get(context) as VM
-//
-//    override fun <VM : ViewModel> create(
-//        modelClass: Class<VM>
-//    ): VM = modelClass.kt.get(context) as VM
-//
-//}

+ 0 - 42
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/StateContextWrapper.kt

@@ -1,42 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.base.helpers
-
-import android.content.Context
-import android.content.ContextWrapper
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
-import androidx.lifecycle.ViewModelStore
-import androidx.lifecycle.ViewModelStoreOwner
-import androidx.savedstate.SavedStateRegistry
-import androidx.savedstate.SavedStateRegistryController
-import androidx.savedstate.SavedStateRegistryOwner
-
-// todo
-class StateContextWrapper(
-    baseContext: Context
-) : ContextWrapper(baseContext) , LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
-
-    private val savedStateRegistryController by lazy {
-        SavedStateRegistryController.create(this)
-    }
-
-    override val lifecycle: Lifecycle by lazy {
-        LifecycleRegistry(this)
-    }
-
-    override val viewModelStore: ViewModelStore by lazy {
-        ViewModelStore()
-    }
-
-    override val savedStateRegistry: SavedStateRegistry
-        get() = savedStateRegistryController.savedStateRegistry
-
-}

+ 8 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/media/PlayerControls.kt

@@ -1,3 +1,11 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.base.media
 
 import androidx.compose.runtime.Composable

+ 8 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/EmptyScreen.kt

@@ -1,3 +1,11 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.base.navigation
 
 import androidx.compose.runtime.Composable

+ 6 - 13
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItem.kt

@@ -8,22 +8,15 @@
 
 package org.mjdev.tvapp.base.navigation
 
-/**
- * Menu item helper class.
- *
- * Normally you do not need to use this class, it is used to autogenerate menu items in
- * navigation drawer of an application.
- *
- * @constructor Create [MenuItem]
- * @property menuText
- * @property menuIcon
- * @property route
- */
 data class MenuItem(
 
     val menuText: Any? = null,
     val menuIcon: Any? = null,
+
     var route: String? = null,
-    var pagerIndex: Int? = null
 
-)
+) {
+
+    override fun toString(): String = menuText.toString()
+
+}

+ 15 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItemClickListener.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.navigation
+
+interface MenuItemClickListener {
+
+    fun onMenuItemClick(item: MenuItem)
+
+}

+ 1 - 13
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavGraphBuilderEx.kt

@@ -10,26 +10,14 @@ package org.mjdev.tvapp.base.navigation
 
 import androidx.navigation.NavGraph
 import androidx.navigation.NavGraphBuilder
-import androidx.navigation.NavHostController
 import androidx.navigation.NavigatorProvider
 import org.mjdev.tvapp.base.navigation.EmptyScreen.Companion.ROUTE_NONE
 
-/**
- * Custom nav graph builder.
- *
- * Just extended of [navHostController] for simplify navigation auto creation.
- * @see : [androidx.navigation.NavGraphBuilder]
- *
- * @param provider
- * @param route
- * @constructor Create [NavGraphBuilderEx]
- * @property navHostController
- */
 class NavGraphBuilderEx(
     provider: NavigatorProvider,
     route: String? = null,
     startRoute: String? = null,
-    val navHostController: NavHostController
+    val navHostController: NavHostControllerEx
 ) : NavGraphBuilder(provider, startRoute ?: "none", route) {
 
     var splashDestinationRoute: String? = startRoute

+ 57 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostControllerEx.kt

@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.navigation
+
+import android.content.Context
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.navigation.NavHostController
+
+// todo apply lifecycle
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+class NavHostControllerEx(
+    context: Context
+) : NavHostController(
+    context
+), LifecycleOwner, DefaultLifecycleObserver {
+
+    private val menuClickListeners = mutableListOf<MenuItemClickListener>()
+
+    override val lifecycle: Lifecycle by lazy { LifecycleRegistry(this) }
+
+    init {
+        lifecycle.addObserver(this)
+    }
+
+    override fun onDestroy(owner: LifecycleOwner) {
+        lifecycle.removeObserver(this)
+        clearClickListeners()
+    }
+
+    fun addMenuClickListener(listener: MenuItemClickListener) {
+        menuClickListeners.add(listener)
+    }
+
+    fun removeMenuClickListener(listener: MenuItemClickListener) {
+        menuClickListeners.remove(listener)
+    }
+
+    fun clearClickListeners() {
+        menuClickListeners.clear()
+    }
+
+    fun onMenuItemClick(item: MenuItem) {
+        menuClickListeners.forEach { l ->
+            l.onMenuItemClick(item)
+        }
+    }
+
+}

+ 2 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostEx.kt

@@ -19,11 +19,10 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
 import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.rememberNavController
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.NavExt.createGraph
+import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvapp.base.navigation.Screen.Companion.screen
 
 @TvPreview
@@ -32,7 +31,7 @@ fun NavHostEx(
     modifier: Modifier = Modifier,
     route: String? = null,
     startRoute: String? = null,
-    navController: NavHostController = rememberNavController(),
+    navController: NavHostControllerEx = rememberNavControllerEx(),
     contentAlignment: Alignment = Alignment.Center,
     enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = {
         fadeIn(animationSpec = tween(700))

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/Screen.kt

@@ -98,7 +98,7 @@ open class Screen : NavController.OnDestinationChangedListener {
 
     @Composable
     open fun Compose(
-        navController: NavHostController?,
+        navController: NavHostControllerEx?,
         backStackEntry: NavBackStackEntry?,
         menuItems: List<MenuItem>,
         args: Map<String, Any?>

+ 15 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/ScreenWithPages.kt

@@ -1,20 +1,28 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.base.navigation
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.res.stringResource
 import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.page.Pager
 import org.mjdev.tvapp.base.page.PagerScope
-import org.mjdev.tvapp.base.page.PagerState
 import org.mjdev.tvapp.base.ui.components.complex.ScreenView
 
 open class ScreenWithPages : Screen() {
 
-    open val pages : @Composable PagerScope.(pagerState: PagerState) -> Unit = {}
+    open val startPage: Int = 0
+
+    open val pages: @Composable PagerScope.() -> Unit = {}
 
     @TvPreview
     @Composable
@@ -22,7 +30,7 @@ open class ScreenWithPages : Screen() {
 
     @Composable
     override fun Compose(
-        navController: NavHostController?,
+        navController: NavHostControllerEx?,
         backStackEntry: NavBackStackEntry?,
         menuItems: List<MenuItem>,
         args: Map<String, Any?>
@@ -37,14 +45,15 @@ open class ScreenWithPages : Screen() {
         ) { screenState ->
 
             Pager(
-                navController=navController,
+                navController = navController,
                 screenState = screenState,
                 menuItems = menuState,
-                startIndex = 0,
+                startIndex = startPage,
                 pages = pages
             )
 
         }
 
     }
+
 }

+ 15 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/page/Page.kt

@@ -1,10 +1,20 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.base.page
 
 import android.annotation.SuppressLint
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.pager.PagerState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
@@ -15,23 +25,21 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import androidx.navigation.NavHostController
 import androidx.tv.material3.Text
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
-import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.state.ScreenState
 
 @Suppress("LeakingThis")
 open class Page {
 
-    var navController: NavHostController? = null
+    var navController: NavHostControllerEx? = null
     var screenState: ScreenState? = null
 
+    @OptIn(ExperimentalFoundationApi::class)
     lateinit var pagerState: PagerState
 
-    val menuItem get() = title?.let { t -> MenuItem(t, icon) }
-
     open val title: Any? = null
     open val icon: Any? = null
 
@@ -70,6 +78,8 @@ open class Page {
 
     }
 
+    override fun toString(): String = title.toString()
+
     companion object {
 
         val EMPTY_PAGE: Page

+ 0 - 11
app/src/main/kotlin/org/mjdev/tvapp/base/page/PageState.kt

@@ -1,11 +0,0 @@
-package org.mjdev.tvapp.base.page
-
-//class PageState(
-//    title: Any? = null,
-//    icon: Any? = null
-//) {
-//
-//    val title: MutableState<Any?> = mutableStateOf(title)
-//    val icon: MutableState<Any?> = mutableStateOf(icon)
-//
-//}

+ 45 - 19
app/src/main/kotlin/org/mjdev/tvapp/base/page/Pager.kt

@@ -1,64 +1,90 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.base.page
 
 import android.annotation.SuppressLint
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.pager.VerticalPager
+import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.navigation.NavHostController
+import kotlinx.coroutines.launch
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
+import org.mjdev.tvapp.base.navigation.MenuItemClickListener
 import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.page.Page.Companion.EMPTY_PAGE
 import org.mjdev.tvapp.base.state.ScreenState
 
+@OptIn(ExperimentalFoundationApi::class)
 @SuppressLint("AutoboxingStateValueProperty")
 @TvPreview
 @Composable
 fun Pager(
-    navController: NavHostController? = null,
+    navController: NavHostControllerEx? = null,
     screenState: ScreenState? = null,
     menuItems: MutableList<MenuItem> = mutableListOf(),
     startIndex: Int = 0,
     roundCornerSize: Dp = 0.dp,
     backGroundColor: Color = Color.Transparent,
     backGroundShape: Shape = RoundedCornerShape(roundCornerSize),
-    pages: @Composable PagerScope.(pagerState: PagerState) -> Unit = {}
+    pages: @Composable PagerScope.() -> Unit = {}
 ) {
 
+    val coroutineScope = rememberCoroutineScope()
+
     val isEdit = isEditMode()
-    val pagerScope = PagerScope(navController, screenState, menuItems)
-    val pageState = remember { PagerState(startIndex) }
 
-    Column(
+    val pagerScope = PagerScope(navController, screenState, menuItems).apply {
+        pages.invoke(this)
+    }
+
+    val pagerState = rememberPagerState(
+        pageCount = { pagerScope.size },
+        initialPage = startIndex,
+    )
+
+    val listener = object : MenuItemClickListener {
+        override fun onMenuItemClick(item: MenuItem) {
+            coroutineScope.launch {
+                pagerState.animateScrollToPage(menuItems.indexOf(item))
+            }
+        }
+    }
+
+    navController?.addMenuClickListener(listener)
+
+    VerticalPager(
         modifier = Modifier
             .fillMaxSize()
             .background(backGroundColor, backGroundShape),
-        verticalArrangement = Arrangement.Center,
-        horizontalAlignment = Alignment.CenterHorizontally,
-    ) {
-
-        pages.invoke(pagerScope, pageState)
-
-        val index = pageState.index.value
+        state = pagerState,
+        beyondBoundsPageCount = 0
+    ) { pageIndex ->
 
-        val page = if (pagerScope.size > index) pagerScope[index]
+        val page = if (pagerScope.size > pageIndex) pagerScope[pageIndex]
         else if (isEdit) EMPTY_PAGE
         else null
 
         page?.let { p ->
             p.navController = navController
             p.screenState = screenState
-            p.pagerState = pageState
+            p.pagerState = pagerState
             p.content()
         }
 

+ 17 - 14
app/src/main/kotlin/org/mjdev/tvapp/base/page/PagerScope.kt

@@ -1,11 +1,20 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.base.page
 
-import androidx.navigation.NavHostController
+import org.mjdev.tvapp.base.extensions.ListExt.addUnique
 import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.state.ScreenState
 
 class PagerScope(
-    val navController: NavHostController? = null,
+    val navController: NavHostControllerEx? = null,
     private val screenState: ScreenState? = null,
     private val menuItems: MutableList<MenuItem> = mutableListOf(),
 ) : ArrayList<Page>() {
@@ -13,20 +22,14 @@ class PagerScope(
     fun page(page: Page) {
         page.navController = navController
         page.screenState = screenState
-        page.menuItem?.also { menuItem ->
-            menuItems.firstOrNull { mi -> mi.menuText == page.title }.also { duplicate ->
-                if (duplicate != null) menuItems.remove(duplicate)
-            }
-            menuItems.add(menuItem)
-        }
         addUnique(page)
-    }
-
-    private fun addUnique(page: Page) {
-        firstOrNull { p -> p.title == page.title }.also { duplicate ->
-            if (duplicate != null) remove(duplicate)
+        page.let { p ->
+            MenuItem(p.title, p.icon)
+        }.also { menuItem ->
+            menuItems.addUnique(menuItem)
         }
-        add(page)
     }
 
+
+
 }

+ 27 - 27
app/src/main/kotlin/org/mjdev/tvapp/base/page/PagerState.kt

@@ -1,27 +1,27 @@
-package org.mjdev.tvapp.base.page
-
-import android.annotation.SuppressLint
-import androidx.compose.runtime.mutableIntStateOf
-
-@Suppress("MemberVisibilityCanBePrivate", "unused")
-@SuppressLint("AutoboxingStateValueProperty")
-class PagerState(
-    val startIndex: Int = 0
-) {
-
-    val history = mutableListOf<Int>()
-    val index = mutableIntStateOf(startIndex)
-    val currentPage get() = if (history.size > 0) history[history.size - 1] else startIndex
-
-
-    fun goToPage(pageIndex: Int) {
-        history.add(currentPage)
-        index.value = pageIndex
-    }
-
-    fun goBack() {
-        history.removeAt(history.size-1)
-        index.value = currentPage
-    }
-
-}
+//package org.mjdev.tvapp.base.page
+//
+//import android.annotation.SuppressLint
+//import androidx.compose.runtime.mutableIntStateOf
+//
+//@Suppress("MemberVisibilityCanBePrivate", "unused")
+//@SuppressLint("AutoboxingStateValueProperty")
+//class PagerState(
+//    val startIndex: Int = 0
+//) {
+//
+//    val history = mutableListOf<Int>()
+//    val index = mutableIntStateOf(startIndex)
+//    val currentPage get() = if (history.size > 0) history[history.size - 1] else startIndex
+//
+//
+//    fun goToPage(pageIndex: Int) {
+//        history.add(currentPage)
+//        index.value = pageIndex
+//    }
+//
+//    fun goBack() {
+//        history.removeAt(history.size-1)
+//        index.value = currentPage
+//    }
+//
+//}

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

@@ -0,0 +1,51 @@
+/*
+ * 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.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.ProvideTextStyle
+
+@Composable
+fun CardContent(
+    modifier: Modifier = Modifier,
+    title: @Composable () -> Unit,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    subtitleAlpha: Float = 0.6f,
+    descriptionAlpha: Float = 0.8f,
+) {
+
+    Column(
+        modifier = modifier
+    ) {
+
+        ProvideTextStyle(MaterialTheme.typography.titleMedium) {
+            title.invoke()
+        }
+
+        ProvideTextStyle(MaterialTheme.typography.bodySmall) {
+            Box(Modifier.graphicsLayer { alpha = subtitleAlpha }) {
+                subtitle.invoke()
+            }
+        }
+
+        ProvideTextStyle(MaterialTheme.typography.bodySmall) {
+            Box(Modifier.graphicsLayer { alpha = descriptionAlpha }) {
+                description.invoke()
+            }
+        }
+
+    }
+
+}

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

@@ -50,10 +50,11 @@ fun ErrorMessage(
     paddingMessage: Dp = paddingTitle,
     backgroundColor: Color = Color.Red,
     roundSize: Dp = 8.dp,
-    cancelText: Any? = stringResource(id = R.string.dismiss),
+    cancelText: Any? = stringResource(id = R.string.bt_dismiss),
     dismissible: Boolean = true,
     onDismiss: () -> Unit = {}
 ) {
+
     Box(
         modifier = Modifier.fillMaxWidth()
     ) {

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

@@ -1,3 +1,11 @@
+/*
+ * 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.complex
 
 import android.annotation.SuppressLint

+ 0 - 83
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ListWithPreview.kt

@@ -1,83 +0,0 @@
-///*
-// * 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.complex
-//
-//import androidx.compose.foundation.border
-//import androidx.compose.foundation.clickable
-//import androidx.compose.foundation.layout.Arrangement
-//import androidx.compose.foundation.layout.Row
-//import androidx.compose.foundation.layout.fillMaxWidth
-//import androidx.compose.foundation.layout.height
-//import androidx.compose.foundation.layout.width
-//import androidx.compose.runtime.Composable
-//import androidx.compose.runtime.mutableStateOf
-//import androidx.compose.runtime.remember
-//import androidx.compose.ui.Modifier
-//import androidx.compose.ui.focus.onFocusChanged
-//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.ExperimentalTvMaterial3Api
-//import androidx.tv.material3.ImmersiveList
-//import org.mjdev.tvapp.base.interfaces.ItemWithImage
-//import org.mjdev.tvapp.base.ui.components.image.ImageAny
-//import timber.log.Timber
-//
-//// todo
-//@OptIn(ExperimentalTvMaterial3Api::class)
-//@Preview
-//@Composable
-//fun <T> ListWithPreview(
-//    modifier: Modifier = Modifier,
-//    items: List<T> = listOf(),
-//    immersiveListHeight: Dp = 300.dp,
-//    cardSpacing: Dp = 10.dp,
-//    cardWidth: Dp = 200.dp,
-//    cardHeight: Dp = 150.dp
-//) {
-//    val itemBackground: (index: Int) -> Any? = { index ->
-//        (if (items.size > index)
-//            (items[index] as? ItemWithImage?)?.backgroundImageUrl
-//        else null) ?: Color.Transparent
-//    }
-//    ImmersiveList(
-//        modifier = modifier
-//            .height(immersiveListHeight + cardHeight / 2)
-//            .fillMaxWidth(),
-//        background = { index, _ ->
-//            ImageAny(
-//                modifier = Modifier
-//                    .height(immersiveListHeight)
-//                    .fillMaxWidth(),
-//                src = itemBackground.invoke(index)
-//            )
-//        }
-//    ) {
-//        Row(horizontalArrangement = Arrangement.spacedBy(cardSpacing)) {
-//            items.forEachIndexed { index, item ->
-//                val isFocused = remember { mutableStateOf(false) }
-//                ImageAny(
-//                    modifier = Modifier
-//                        .width(cardWidth)
-//                        .height(cardHeight)
-//                        .border(5.dp, Color.White.copy(alpha = if (isFocused.value) 1f else 0.3f))
-//                        .onFocusChanged { focusState ->
-//                            isFocused.value = focusState.isFocused
-//                        }
-//                        .immersiveListItem(index)
-//                        .clickable {
-//                            Timber.tag("ImmersiveList: Item $index was clicked")
-//                        },
-//                    src = itemBackground.invoke(index)
-//                )
-//            }
-//        }
-//    }
-//}

+ 0 - 97
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/MyComposeView.kt

@@ -1,97 +0,0 @@
-///*
-// * Copyright (c) Milan Jurkulák 2023.
-// * Contact:
-// * e: mimoccc@gmail.com
-// * e: mj@mjdev.org
-// * w: https://mjdev.org
-// */
-//
-//package org.mjdev.tvapp.ui.components
-//
-//import android.content.Context
-//import android.graphics.PixelFormat
-//import android.view.Gravity
-//import android.view.View
-//import android.view.WindowManager
-//import androidx.compose.runtime.Composable
-//import androidx.compose.ui.platform.AbstractComposeView
-//import androidx.lifecycle.Lifecycle
-//import androidx.lifecycle.LifecycleOwner
-//import androidx.lifecycle.LifecycleRegistry
-//import androidx.lifecycle.ViewModelStore
-//import androidx.lifecycle.ViewModelStoreOwner
-//import androidx.lifecycle.setViewTreeLifecycleOwner
-//import androidx.lifecycle.setViewTreeViewModelStoreOwner
-//import androidx.savedstate.SavedStateRegistry
-//import androidx.savedstate.SavedStateRegistryController
-//import androidx.savedstate.SavedStateRegistryOwner
-//import androidx.savedstate.setViewTreeSavedStateRegistryOwner
-//
-//class MyComposeView(
-//    val composeView: View
-//) : AbstractComposeView(
-//    composeView.context
-//), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
-//
-//    private val savedStateRegistryController by lazy {
-//        SavedStateRegistryController.create(this)
-//    }
-//
-//    override val lifecycle: Lifecycle by lazy {
-//        LifecycleRegistry(this)
-//    }
-//
-//    override val viewModelStore: ViewModelStore by lazy {
-//        ViewModelStore()
-//    }
-//
-//    override val savedStateRegistry: SavedStateRegistry
-//        get() = savedStateRegistryController.savedStateRegistry
-//
-//
-//    private var viewShowing: Boolean = false
-//
-//    private fun handleLifecycleEvent(event: Lifecycle.Event) =
-//        (lifecycle as LifecycleRegistry).handleLifecycleEvent(event)
-//
-//
-//    private val windowManager by lazy {
-//        composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-//    }
-//
-//    private var params: WindowManager.LayoutParams = WindowManager.LayoutParams()
-//        .apply {
-//            gravity = Gravity.TOP
-//            type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
-//            token = composeView.applicationWindowToken
-//            width = WindowManager.LayoutParams.WRAP_CONTENT
-//            height = WindowManager.LayoutParams.WRAP_CONTENT
-//            format = PixelFormat.TRANSLUCENT
-//            flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
-//        }
-//
-//    init {
-//        composeView.setViewTreeLifecycleOwner(this)
-//        composeView.setViewTreeViewModelStoreOwner(this)
-//        composeView.setViewTreeSavedStateRegistryOwner(this)
-//    }
-//
-//    @Composable
-//    override fun Content() {
-//        // Some Compose Drawing Function
-//    }
-//
-//    fun show() {
-//        if (viewShowing) dismiss()
-//        windowManager.addView(this, params)
-//        viewShowing = true
-//    }
-//
-//    fun dismiss() {
-//        if (!viewShowing) return
-//        disposeComposition()
-//        windowManager.removeViewImmediate(this)
-//        viewShowing = false
-//    }
-//
-//}

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

@@ -17,11 +17,11 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.navigation.NavHostController
 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.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.base.state.ScreenState
 import org.mjdev.tvapp.base.state.ScreenState.Companion.rememberScreenState
@@ -31,7 +31,7 @@ import org.mjdev.tvapp.base.ui.components.navigation.Navigation
 @TvPreview
 @Composable
 fun ScreenView(
-    navController: NavHostController? = null,
+    navController: NavHostControllerEx? = null,
     navigationBackgroundColor:Color = Color(0xff202020),
     actions: @Composable RowScope.() -> Unit = {},
     title: Any? = R.string.app_name,

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

@@ -26,7 +26,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.navigation.NavHostController
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.NavigationDrawer
@@ -34,17 +33,19 @@ import androidx.tv.material3.rememberDrawerState
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
 import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 
 const val SETTINGS_ITEM = 65535
 const val SEARCH_ITEM = 65534
 
+@Suppress("LocalVariableName")
 @SuppressLint("AutoboxingStateValueProperty")
 @OptIn(ExperimentalTvMaterial3Api::class)
 @TvPreview
 @Composable
 fun Navigation(
     modifier: Modifier = Modifier,
-    navController: NavHostController? = null,
+    navController: NavHostControllerEx? = null,
     items: List<MenuItem> = listOf(),
     backgroundColor: Color = Color.DarkGray,
     roundSize: Dp = 8.dp,
@@ -56,6 +57,8 @@ fun Navigation(
     settingsArrangement: Arrangement.Vertical = Arrangement.Bottom,
     onSettingsItemFocused: () -> Unit = {},
     onSearchItemFocused: () -> Unit = {},
+    onSettingsItemClicked: () -> Unit = {},
+    onSearchItemClicked: () -> Unit = {},
     onDrawerItemClick: (id: Int) -> Unit = {},
     onDrawerItemFocused: (id: Int) -> Unit = {},
     content: @Composable () -> Unit = {}
@@ -65,6 +68,28 @@ fun Navigation(
     val drawerState = rememberDrawerState(if (isEdit) DrawerValue.Open else DrawerValue.Closed)
     val focused = remember { mutableIntStateOf(-1) }
 
+    val _onDrawerItemClick: (id: Int) -> Unit = { id ->
+        when (id) {
+            SETTINGS_ITEM -> onSettingsItemClicked()
+            SEARCH_ITEM -> onSearchItemClicked()
+            else -> items[id].let { menuItem ->
+                navController?.onMenuItemClick(menuItem)
+                onDrawerItemClick(id)
+            }
+        }
+    }
+
+    val _onDrawerItemFocus: (id: Int) -> Unit = { id ->
+        when (id) {
+            SETTINGS_ITEM -> onSettingsItemFocused()
+            SEARCH_ITEM -> onSearchItemFocused()
+            else -> items[id].let { menuItem ->
+                navController?.onMenuItemClick(menuItem)
+                onDrawerItemFocused(id)
+            }
+        }
+    }
+
     NavigationDrawer(
         modifier = modifier
             .fillMaxHeight()
@@ -95,10 +120,10 @@ fun Navigation(
                             onFocus = { id ->
                                 drawerState.setValue(DrawerValue.Open)
                                 focused.value = id
-                                onDrawerItemFocused(id)
+                                _onDrawerItemFocus(id)
                             },
                             onClick = { id ->
-                                onDrawerItemClick(id)
+                                _onDrawerItemClick(id)
                             }
                         )
 
@@ -121,10 +146,10 @@ fun Navigation(
                             onFocus = { id ->
                                 drawerState.setValue(DrawerValue.Open)
                                 focused.value = id
-                                onDrawerItemFocused(id)
+                                _onDrawerItemFocus(id)
                             },
                             onClick = { id ->
-                                onDrawerItemClick(id)
+                                _onDrawerItemClick(id)
                             }
                         )
 
@@ -147,10 +172,10 @@ fun Navigation(
                             onFocus = { id ->
                                 drawerState.setValue(DrawerValue.Open)
                                 focused.value = id
-                                onDrawerItemFocused(id)
+                                _onDrawerItemFocus(id)
                             },
                             onClick = { id ->
-                                onDrawerItemClick(id)
+                                _onDrawerItemClick(id)
                             }
                         )
                     }

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

@@ -1,33 +1,104 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.ui.pages
 
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Info
 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.page.Page
+import org.mjdev.tvapp.base.ui.components.card.CardContent
 import org.mjdev.tvapp.base.ui.components.image.CircleImage
+import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 class AboutPage : Page() {
 
     override val title: Int = R.string.title_about
     override val icon: ImageVector = Icons.Default.Info
 
+    @OptIn(ExperimentalTvMaterial3Api::class)
     @TvPreview
     @Composable
     override fun Content() {
 
-        Column {
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(16.dp)
+        ) {
 
-            CircleImage(
+            Card(
+                onClick = {},
+                onLongClick = {},
                 modifier = Modifier.fillMaxSize(),
-                src = Color.Black
-            )
+                shape = CardDefaults.shape(),
+                colors = CardDefaults.colors(
+                    containerColor = Color(0xff242424)
+                ),
+            ) {
+
+                Row(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .padding(16.dp),
+                    horizontalArrangement = Arrangement.Start,
+                    verticalAlignment = Alignment.CenterVertically,
+                    content = {
+
+                        CircleImage(
+                            modifier = Modifier.size(128.dp),
+                            contentPadding = 10.dp,
+                            src = R.drawable.milanj
+                        )
+
+                        CardContent(
+                            modifier = Modifier.fillMaxWidth(),
+                            title = {
+                                TextAny(
+                                    color = Color.White,
+                                    text = R.string.app_name
+                                )
+                            },
+                            subtitle = {
+                                TextAny(
+                                    color = Color.White,
+                                    text = R.string.text_app_description
+                                )
+                            },
+                            description = {
+                                TextAny(
+                                    color = Color.White,
+                                    text = R.string.author
+                                )
+                            }
+                        )
+
+                    }
+                )
+
+            }
 
         }
 

+ 12 - 5
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

@@ -1,3 +1,11 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.ui.pages
 
 import android.annotation.SuppressLint
@@ -42,7 +50,6 @@ import org.mjdev.tvapp.base.ui.components.complex.Header
 import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.PlayerScreen
 import org.mjdev.tvapp.viewmodel.MainViewModel
-import org.mjdev.tvapp.base.extensions.ComposeExt.collectAsState
 
 @SuppressLint("ComposableNaming")
 class MainPage : Page() {
@@ -67,7 +74,7 @@ class MainPage : Page() {
 
         val onItemClick: (movie: Movie?) -> Unit = { movie ->
             if (movie == null) {
-                viewModel?.postError("No media found.".asException())
+                viewModel.postError("No media found.".asException())
             } else if (movie.hasVideoUri) {
                 navController?.open<PlayerScreen>(movie.id)
             } else {
@@ -75,7 +82,7 @@ class MainPage : Page() {
             }
         }
 
-        viewModel?.handleError { error ->
+        viewModel.handleError { error ->
             screenState?.error(error)
         }
 
@@ -91,7 +98,7 @@ class MainPage : Page() {
 
                 Header(
                     title = screenState?.titleState?.value,
-                    messagesCount = messages?.value?.size ?: 0
+                    messagesCount = messages.value.size
                 )
 
             }
@@ -119,7 +126,7 @@ class MainPage : Page() {
                 )
             }
 
-            items(categoryList?.value ?: emptyList()) { category ->
+            items(categoryList.value) { category ->
 
                 Text(
                     modifier = Modifier

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

@@ -1,36 +1,94 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
 package org.mjdev.tvapp.ui.pages
 
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ShoppingCart
 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.page.Page
-import org.mjdev.tvapp.base.ui.components.image.CircleImage
+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
 
+    @OptIn(ExperimentalTvMaterial3Api::class)
     @TvPreview
     @Composable
     override fun Content () {
 
-        Column(
-            Modifier.fillMaxSize()
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(16.dp)
         ) {
 
-            CircleImage(
-                src = R.drawable.milanj
-            )
+            Card(
+                onClick = {},
+                onLongClick = {},
+                modifier = Modifier.fillMaxSize(),
+                shape = CardDefaults.shape(),
+                colors = CardDefaults.colors(
+                    containerColor = Color(0xff242424)
+                ),
+            ) {
 
-        }
+                Row(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .padding(16.dp),
+                    horizontalArrangement = Arrangement.Start,
+                    verticalAlignment = Alignment.CenterVertically,
+                    content = {
+
+                        CardContent(
+                            modifier = Modifier.fillMaxWidth(),
+                            title = {
+                                TextAny(
+                                    color = Color.White,
+                                    text = R.string.app_name
+                                )
+                            },
+                            subtitle = {
+                                TextAny(
+                                    color = Color.White,
+                                    text = R.string.text_unimplemented_yet
+                                )
+                            },
+                            description = {
+                            }
+                        )
 
+                    }
+                )
+
+            }
+
+        }
     }
 
 }

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

@@ -15,7 +15,6 @@ import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.remember
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
 import androidx.navigation.NavType
 import androidx.navigation.navArgument
 import org.mjdev.tvapp.R
@@ -23,6 +22,7 @@ import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.HiltExt.appViewModel
 import org.mjdev.tvapp.base.extensions.StringExt.asException
 import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.state.DetailsLoadingState
 import org.mjdev.tvapp.ui.components.Details
@@ -52,7 +52,7 @@ class DetailScreen : Screen() {
 
     @Composable
     override fun Compose(
-        navController: NavHostController?,
+        navController: NavHostControllerEx?,
         backStackEntry: NavBackStackEntry?,
         menuItems: List<MenuItem>,
         args: Map<String, Any?>

+ 4 - 3
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt

@@ -12,21 +12,22 @@ import androidx.compose.runtime.Composable
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.navigation.ScreenWithPages
 import org.mjdev.tvapp.base.page.PagerScope
-import org.mjdev.tvapp.base.page.PagerState
 import org.mjdev.tvapp.ui.pages.AboutPage
 import org.mjdev.tvapp.ui.pages.MainPage
 import org.mjdev.tvapp.ui.pages.SubscriptionPage
 
 class MainScreen : ScreenWithPages() {
 
+    override val startPage: Int = 1
+
     @TvPreview
     @Composable
     override fun Compose() = super.Compose()
 
-    override val pages: @Composable (PagerScope.(PagerState) -> Unit) = {
+    override val pages: @Composable (PagerScope.() -> Unit) = {
 
-        page(MainPage())
         page(AboutPage())
+        page(MainPage())
         page(SubscriptionPage())
 
     }

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

@@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
 import androidx.navigation.NavType
 import androidx.navigation.navArgument
 import org.mjdev.tvapp.R
@@ -25,6 +24,7 @@ import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.HiltExt.appViewModel
 import org.mjdev.tvapp.base.extensions.StringExt.asException
 import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.state.DetailsLoadingState
 import org.mjdev.tvapp.ui.components.Loading
@@ -54,7 +54,7 @@ class PlayerScreen : Screen() {
 
     @Composable
     override fun Compose(
-        navController: NavHostController?,
+        navController: NavHostControllerEx?,
         backStackEntry: NavBackStackEntry?,
         menuItems: List<MenuItem>,
         args: Map<String, Any?>

+ 14 - 3
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt

@@ -24,18 +24,21 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.scale
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.navigation.NavBackStackEntry
-import androidx.navigation.NavHostController
+import androidx.tv.material3.MaterialTheme
 import kotlinx.coroutines.delay
 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.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.base.ui.components.icon.IconAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
@@ -50,7 +53,7 @@ class SplashScreen : Screen() {
 
     @Composable
     override fun Compose(
-        navController: NavHostController?,
+        navController: NavHostControllerEx?,
         backStackEntry: NavBackStackEntry?,
         menuItems: List<MenuItem>,
         args: Map<String, Any?>
@@ -92,7 +95,15 @@ class SplashScreen : Screen() {
                 text = R.string.app_name,
                 fontSize = 64.sp,
                 fontWeight = FontWeight.Bold,
-                color = Color.White
+                color = Color.White,
+                modifier = Modifier.padding(10.dp),
+                style = MaterialTheme.typography.bodyMedium.copy(
+                    shadow = Shadow(
+                        color = Color.Gray,
+                        offset = Offset(5f, 5f),
+                        blurRadius = 10f
+                    )
+                ),
             )
 
         }

+ 8 - 6
app/src/main/res/values/strings.xml

@@ -12,19 +12,21 @@
 
     <string name="app_name">Tv App</string>
 
-    <string name="title_movie_detail">Movie detail</string>
     <string name="title_about">About</string>
+    <string name="title_error">Error</string>
+    <string name="title_home">Home</string>
+    <string name="title_movie_detail">Movie detail</string>
     <string name="title_movie_player">Movie player</string>
     <string name="title_subscription">Subscription</string>
-    <string name="title_error">Error</string>
 
+    <string name="error_no_network">Internet connection is missing.</string>
     <string name="error_unknown">Unknown error.</string>
 
-    <string name="dismiss">Dismiss</string>
+    <string name="bt_dismiss">Dismiss</string>
 
-    <string name="error_no_network">Internet connection is missing.</string>
+    <string name="text_app_description">\n\n\nSimple TV Application for AndroidTV or SetTopBox with Android or any tablet or mobile device.\nAll in one solution for mobile app responsive to device.\nThis app is just an simple demonstration of Jetpack Compose and all other modern architecture components\n\n\n</string>
+    <string name="text_unimplemented_yet">This feature is not yet implemented.</string>
 
-    <string name="author">Created by Milan Jurkulak (C) 2023</string>
-    <string name="title_home">Home</string>
+    <string name="author">Created by Milan Jurkulak © 2023</string>
 
 </resources>

+ 1 - 1
gradle.properties

@@ -23,4 +23,4 @@ kotlin.code.style=official
 android.nonTransitiveRClass=true
 android.defaults.buildfeatures.buildconfig=true
 android.nonFinalResIds=false
-org.gradle.unsafe.configuration-cache=false
+org.gradle.unsafe.configuration-cache=true