Kaynağa Gözat

code improvements

Milan Jurkulak 3 yıl önce
ebeveyn
işleme
bb21aa59e8
59 değiştirilmiş dosya ile 1118 ekleme ve 970 silme
  1. 77 0
      app/src/main/kotlin/org/mjdev/tvapp/activity/ActivityResultHandler.kt
  2. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt
  3. 27 106
      app/src/main/kotlin/org/mjdev/tvapp/base/activity/ComposableActivity.kt
  4. 28 0
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/AnimExt.kt
  5. 5 1
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/DrawableExt.kt
  6. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/NavExt.kt
  7. 0 22
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItem.kt
  8. 0 15
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItemClickListener.kt
  9. 0 57
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostControllerEx.kt
  10. 0 60
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/ScreenWithPages.kt
  11. 0 35
      app/src/main/kotlin/org/mjdev/tvapp/base/page/PagerScope.kt
  12. 0 106
      app/src/main/kotlin/org/mjdev/tvapp/base/state/ScreenState.kt
  13. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/CarouselCard.kt
  14. 6 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Badge.kt
  15. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ErrorMessage.kt
  16. 3 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableBox.kt
  17. 12 10
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableCard.kt
  18. 5 7
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Header.kt
  19. 26 42
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScreenView.kt
  20. 3 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Tabs.kt
  21. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/UserPic.kt
  22. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/icon/IconAny.kt
  23. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/image/CircleImage.kt
  24. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/image/ImageAny.kt
  25. 5 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/IMediaPlayer.kt
  26. 6 6
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerContainer.kt
  27. 5 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerContainerDefaults.kt
  28. 5 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerImpl.kt
  29. 5 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerState.kt
  30. 5 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/NativePlayerImpl.kt
  31. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/PlayerControls.kt
  32. 5 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/EmptyScreen.kt
  33. 85 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/MenuItem.kt
  34. 15 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/MenuItemClickListener.kt
  35. 157 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavDrawerContent.kt
  36. 6 7
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavGraphBuilderEx.kt
  37. 101 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavHostControllerEx.kt
  38. 6 6
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavHostEx.kt
  39. 36 148
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt
  40. 13 13
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavigationRow.kt
  41. 58 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavigationState.kt
  42. 46 64
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Screen.kt
  43. 45 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/ScreenWithPages.kt
  44. 25 46
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Page.kt
  45. 34 36
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Pager.kt
  46. 61 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/PagerScope.kt
  47. 0 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/PagerState.kt
  48. 30 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/SearchPage.kt
  49. 30 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/SettingsPage.kt
  50. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/text/CircleText.kt
  51. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/MediaPlayer.kt
  52. 1 6
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/AboutPage.kt
  53. 13 21
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  54. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/SubscriptionPage.kt
  55. 8 10
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  56. 17 4
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt
  57. 18 23
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/PlayerScreen.kt
  58. 54 59
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt
  59. 13 8
      app/src/main/res/values/strings.xml

+ 77 - 0
app/src/main/kotlin/org/mjdev/tvapp/activity/ActivityResultHandler.kt

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.activity
+
+import android.content.Intent
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import org.mjdev.tvapp.base.activity.ComposableActivity
+
+/**
+ * Activity result handler.
+ *
+ * @param I
+ * @constructor Create [ActivityResultHandler]
+ * @property lifecycle lifecycle handler
+ * @property onLaunch function to start activity
+ * @property onActivityResult result handler
+ */
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+class ActivityResultHandler<I>(
+    private val activity: ComposableActivity,
+    val lifecycle: Lifecycle,
+    val onLaunch: (args: List<I>) -> Unit,
+    val onActivityResult: (requestCode: Int, resultCode: Int, intent: Intent?) -> Unit,
+) {
+
+    private val observer = LifecycleEventObserver { _, event ->
+        when (event) {
+            Lifecycle.Event.ON_DESTROY -> {
+                unregister()
+            }
+
+            else -> {}
+        }
+    }
+
+    init {
+        lifecycle.addObserver(observer)
+        activity.activityResultListeners.add(this)
+    }
+
+    /**
+     * Function to register result handler.
+     * It is called automatically upon lifecycle state.
+     * */
+    fun unregister() {
+        lifecycle.removeObserver(observer)
+        activity.activityResultListeners.remove(this)
+    }
+
+    /**
+     * Post result to handler.
+     *
+     * @param requestCode Request code
+     * @param resultCode Result code
+     * @param intent Intent result data
+     */
+    fun postResult(requestCode: Int, resultCode: Int, intent: Intent?) {
+        onActivityResult(requestCode, resultCode, intent)
+    }
+
+    /**
+     * Launch the onLaunch handler.
+     *
+     * @param args Args
+     */
+    fun launch(vararg args: I) {
+        onLaunch(args.toList())
+    }
+
+}

+ 2 - 2
app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt

@@ -12,8 +12,8 @@ import androidx.compose.runtime.Composable
 import dagger.hilt.android.AndroidEntryPoint
 import org.mjdev.tvapp.base.activity.ComposableActivity
 import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.navigation.NavGraphBuilderEx
-import org.mjdev.tvapp.base.navigation.Screen.Companion.screen
+import org.mjdev.tvapp.base.ui.components.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.screen
 import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.MainScreen
 import org.mjdev.tvapp.ui.screens.PlayerScreen

+ 27 - 106
app/src/main/kotlin/org/mjdev/tvapp/base/activity/ComposableActivity.kt

@@ -13,89 +13,80 @@ import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.annotation.CallSuper
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import androidx.core.view.WindowCompat
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.NonInteractiveSurfaceDefaults
 import androidx.tv.material3.Surface
+import org.mjdev.tvapp.activity.ActivityResultHandler
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ContextExt.isATv
-import org.mjdev.tvapp.base.navigation.EmptyScreen
-import org.mjdev.tvapp.base.navigation.NavGraphBuilderEx
-import org.mjdev.tvapp.base.navigation.NavHostEx
-import org.mjdev.tvapp.base.navigation.Screen.Companion.screen
+import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
+import org.mjdev.tvapp.base.ui.components.navigation.EmptyScreen
+import org.mjdev.tvapp.base.ui.components.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.base.ui.components.navigation.NavHostEx
+import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.screen
 import org.mjdev.tvapp.ui.theme.TVAppTheme
 import timber.log.Timber
 
-/**
- * Composable activity.
- *
- * Activity that holds all the logic for navigation, to simplify it.
- *
- * @constructor Create [ComposableActivity]
- * @property navGraphBuilder
- */
-@Suppress("MemberVisibilityCanBePrivate", "unused")
+@Suppress("MemberVisibilityCanBePrivate", "unused", "LeakingThis")
 open class ComposableActivity : ComponentActivity() {
 
-    /**
-     * Custom activity result listeners
-     */
     val activityResultListeners = mutableListOf<ActivityResultHandler<*>>()
 
     open val navGraphBuilder: NavGraphBuilderEx.() -> Unit = {
         screen(route = EmptyScreen())
     }
 
+    open val backgroundColor = Color(0xff202020)
+    open val roundCornerSize: Dp = 16.dp
+    open val backgroundShape: Shape = RoundedCornerShape(roundCornerSize)
+
     @TvPreview
     @OptIn(ExperimentalTvMaterial3Api::class)
     @Composable
     @CallSuper
     open fun Compose() {
 
-        TVAppTheme {
+        val navController = rememberNavControllerEx()
 
+        TVAppTheme {
             Surface(
-                modifier = Modifier.fillMaxSize(),
+                modifier = Modifier
+                    .background(backgroundColor, backgroundShape)
+                    .fillMaxSize(),
                 shape = RectangleShape,
                 colors = NonInteractiveSurfaceDefaults.colors(
-                    containerColor = Color.Black
+                    containerColor = backgroundColor
                 )
             ) {
-
                 NavHostEx(
                     modifier = Modifier.fillMaxSize(),
+                    navController = navController,
                     builder = navGraphBuilder
                 )
-
             }
-
         }
 
     }
 
-    /**
-     * {@inheritDoc}
-     */
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         WindowCompat.setDecorFitsSystemWindows(window, !isATv)
-        setContent { Compose() }
+        setContent {
+            Compose()
+        }
     }
 
-    /**
-     * On activity result handler.
-     *
-     * @param requestCode Request code
-     * @param resultCode Result code
-     * @param intent Intent
-     */
     @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
     override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
         super.onActivityResult(requestCode, resultCode, intent)
@@ -108,84 +99,14 @@ open class ComposableActivity : ComponentActivity() {
         }
     }
 
-    /**
-     * Register for activity result.
-     *
-     * Custom handler registration.
-     *
-     * @param onLaunch On launch method, to start custom activity.
-     * @param onActivityResult On activity result handler.
-     * @param T T result type
-     * @return [ActivityResultHandler] handler generated
-     */
     fun <T> registerForActivityResult(
         onLaunch: (args: List<T>) -> Unit,
         onActivityResult: (requestCode: Int, resultCode: Int, intent: Intent?) -> Unit
     ): ActivityResultHandler<T> = ActivityResultHandler(
+        this,
         lifecycle,
         onLaunch,
         onActivityResult
     )
 
-    /**
-     * Activity result handler.
-     *
-     * @param I
-     * @constructor Create [ActivityResultHandler]
-     * @property lifecycle lifecycle handler
-     * @property onLaunch function to start activity
-     * @property onActivityResult result handler
-     */
-    @Suppress("unused")
-    inner class ActivityResultHandler<I>(
-        val lifecycle: Lifecycle,
-        val onLaunch: (args: List<I>) -> Unit,
-        val onActivityResult: (requestCode: Int, resultCode: Int, intent: Intent?) -> Unit,
-    ) {
-        val observer = LifecycleEventObserver { _, event ->
-            when (event) {
-                Lifecycle.Event.ON_DESTROY -> {
-                    unregister()
-                }
-
-                else -> {}
-            }
-        }
-
-        init {
-            lifecycle.addObserver(observer)
-            activityResultListeners.add(this)
-        }
-
-        /**
-         * Function to register result handler.
-         * It is called automatically upon lifecycle state.
-         * */
-        fun unregister() {
-            lifecycle.removeObserver(observer)
-            activityResultListeners.remove(this)
-        }
-
-        /**
-         * Post result to handler.
-         *
-         * @param requestCode Request code
-         * @param resultCode Result code
-         * @param intent Intent result data
-         */
-        fun postResult(requestCode: Int, resultCode: Int, intent: Intent?) {
-            onActivityResult(requestCode, resultCode, intent)
-        }
-
-        /**
-         * Launch the onLaunch handler.
-         *
-         * @param args Args
-         */
-        fun launch(vararg args: I) {
-            onLaunch(args.toList())
-        }
-
-    }
-
 }

+ 28 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/AnimExt.kt

@@ -0,0 +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.extensions
+
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+object AnimExt {
+
+    const val ANIMATION_DURATION = 1000
+
+    val SlideInVertically = slideInVertically(animationSpec = tween(ANIMATION_DURATION))
+    val SlideOutVertically = slideOutVertically(animationSpec = tween(ANIMATION_DURATION))
+
+    val FadeIn = fadeIn(animationSpec = tween(ANIMATION_DURATION))
+    val FadeOut = fadeOut(animationSpec = tween(ANIMATION_DURATION))
+
+}

+ 5 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/DrawableExt.kt

@@ -15,9 +15,13 @@ import androidx.core.graphics.drawable.toBitmap
 
 object DrawableExt {
 
+    // todo check width and height
     fun Drawable.asImageBitmap(
         width: Int = intrinsicWidth,
         height: Int = intrinsicHeight
-    ): ImageBitmap = toBitmap(width, height).asImageBitmap()
+    ): ImageBitmap = toBitmap(
+        if (width == 0) 1 else width,
+        if (height == 0) 1 else height
+    ).asImageBitmap()
 
 }

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

@@ -19,8 +19,8 @@ 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
+import org.mjdev.tvapp.base.ui.components.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
 
 object NavExt {
 

+ 0 - 22
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItem.kt

@@ -1,22 +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.navigation
-
-data class MenuItem(
-
-    val menuText: Any? = null,
-    val menuIcon: Any? = null,
-
-    var route: String? = null,
-
-) {
-
-    override fun toString(): String = menuText.toString()
-
-}

+ 0 - 15
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItemClickListener.kt

@@ -1,15 +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.navigation
-
-interface MenuItemClickListener {
-
-    fun onMenuItemClick(item: MenuItem)
-
-}

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

@@ -1,57 +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.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)
-        }
-    }
-
-}

+ 0 - 60
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/ScreenWithPages.kt

@@ -1,60 +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.navigation
-
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.res.stringResource
-import androidx.navigation.NavBackStackEntry
-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.ui.components.complex.ScreenView
-
-open class ScreenWithPages : Screen() {
-
-    open val startPage: Int = 0
-
-    open val pages: @Composable PagerScope.() -> Unit = {}
-
-    @TvPreview
-    @Composable
-    override fun Compose() = super.Compose()
-
-    @Composable
-    override fun Compose(
-        navController: NavHostControllerEx?,
-        backStackEntry: NavBackStackEntry?,
-        menuItems: List<MenuItem>,
-        args: Map<String, Any?>
-    ) {
-
-        val menuState = remember { mutableListOf<MenuItem>().apply { addAll(menuItems) } }
-
-        ScreenView(
-            navController = navController,
-            title = if (titleResId > -1) stringResource(titleResId) else R.string.app_name,
-            menuItems = menuState
-        ) { screenState ->
-
-            Pager(
-                navController = navController,
-                screenState = screenState,
-                menuItems = menuState,
-                startIndex = startPage,
-                pages = pages
-            )
-
-        }
-
-    }
-
-}

+ 0 - 35
app/src/main/kotlin/org/mjdev/tvapp/base/page/PagerScope.kt

@@ -1,35 +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.page
-
-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: NavHostControllerEx? = null,
-    private val screenState: ScreenState? = null,
-    private val menuItems: MutableList<MenuItem> = mutableListOf(),
-) : ArrayList<Page>() {
-
-    fun page(page: Page) {
-        page.navController = navController
-        page.screenState = screenState
-        addUnique(page)
-        page.let { p ->
-            MenuItem(p.title, p.icon)
-        }.also { menuItem ->
-            menuItems.addUnique(menuItem)
-        }
-    }
-
-
-
-}

+ 0 - 106
app/src/main/kotlin/org/mjdev/tvapp/base/state/ScreenState.kt

@@ -1,106 +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.state
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-
-/**
- * Activity view state.
- * Used to keep states and change in activity.
- *
- * @param title
- * @param error
- * @constructor Create [ScreenState]
- */
-@Suppress( "unused")
-class ScreenState(
-    title: Any? = null,
-    error: Throwable? = null
-) {
-
-    val errorState: MutableState<Throwable?> = mutableStateOf(error)
-    val titleState: MutableState<Any?> = mutableStateOf(title)
-
-    /**
-     * Clear error.
-     * */
-    fun clearError() {
-        errorState.value = null
-    }
-
-    /**
-     * Set Error.
-     *
-     * @param message Message of an error
-     */
-    fun error(message: String) {
-        errorState.value = Exception(message)
-    }
-
-    /**
-     * Set Error.
-     *
-     * @param e E Throwable to set error for
-     */
-    fun error(e: Throwable?) {
-        errorState.value = Exception(e)
-    }
-
-    /**
-     * Set info message.
-     *
-     * @param message Message
-     */
-    fun info(message: String) {
-        errorState.value = Exception(message)
-    }
-
-    /**
-     * Set info from throwable.
-     *
-     * @param e E info throwable
-     */
-    fun info(e: Throwable) {
-        errorState.value = Exception(e)
-    }
-
-    /**
-     * Set title.
-     *
-     * @param title Title
-     */
-    fun setTitle(title: Any?) {
-        titleState.value = title
-    }
-
-    companion object {
-
-        /**
-         * Remember activity view state.
-         *
-         * Composable helper
-         *
-         * @param title Title
-         * @param error Error
-         * @return [ScreenState]
-         */
-        @Composable
-        fun rememberScreenState(
-            title: Any? = null,
-            error: Throwable? = null
-        ): ScreenState = remember {
-            ScreenState(title, error)
-        }
-
-    }
-
-}

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

@@ -41,13 +41,14 @@ fun CarouselCard(
 
     CompactCard(
         modifier = modifier,
+        scale = scale,
         image = {
             ImageAny(
                 modifier = modifier,
                 src = movie?.cardImageUrl,
                 contentDescription = movie?.description,
                 contentScale = contentScale,
-                placeholder = placeHolder
+                placeholder = placeHolder,
             )
         },
         title = {
@@ -72,7 +73,6 @@ fun CarouselCard(
             ),
             pressedGlow = Glow.None
         ),
-        scale = CardScale.None,
         onClick = onClick
     )
 

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

@@ -31,8 +31,8 @@ fun Badge(
     shape: Shape = CircleShape,
     count: Int = 0,
     backGroundColor: Color = MaterialTheme.colorScheme.secondary,
-    borderColor: Color = Color.White,
-    borderSize: Dp = 1.dp,
+    borderColor: Color = Color.Transparent,
+    borderSize: Dp = 0.dp,
     textColor: Color = Color.White,
     textSize: TextUnit = 20.sp,
     contentPadding: Dp = 2.dp,
@@ -42,6 +42,9 @@ fun Badge(
 
     FocusableBox(
         modifier = modifier.clip(CircleShape),
+        shape = shape,
+        onFocus = onFocus,
+        onClick = onClick
     ) {
 
         CircleText(
@@ -49,6 +52,7 @@ fun Badge(
             contentPadding = contentPadding + borderSize,
             backGroundColor = backGroundColor,
             text = count.toString(),
+            textSize = textSize,
             textColor = textColor,
             borderColor = borderColor,
             borderSize = borderSize

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

@@ -40,7 +40,7 @@ import org.mjdev.tvapp.base.ui.components.text.TextAny
 @Composable
 fun ErrorMessage(
     error: Throwable? = null,
-    contentPadding: Dp = 8.dp,
+    contentPadding: Dp = 4.dp,
     title: Any? = null,
     fontSizeTitle: TextUnit = 24.sp,
     fontSizeMessage: TextUnit = 14.sp,

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

@@ -11,6 +11,7 @@ package org.mjdev.tvapp.base.ui.components.complex
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
@@ -18,7 +19,6 @@ import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.onFocusEvent
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
@@ -42,7 +42,8 @@ fun FocusableBox(
     unFocusedColor: Color = Color.Transparent,
     pressedColor: Color = focusedColor.copy(alpha = 0.5f),
     disabledColor: Color = Color.Gray.copy(alpha = 0.5f),
-    shape: Shape = RectangleShape,
+    roundCornerSize: Dp = 0.dp,
+    shape: Shape = RoundedCornerShape(roundCornerSize),
     borderColor: Color = Color.Transparent,
     borderSize: Dp = 0.dp,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },

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

@@ -43,7 +43,6 @@ import org.mjdev.tvapp.data.Movie
 fun FocusableCard(
     movie: Movie? = null,
     modifier: Modifier = Modifier,
-    focused: Boolean = false,
     contentScale: ContentScale = ContentScale.Crop,
     placeHolder: Int = R.drawable.placeholder,
     scale: CardScale = CardDefaults.scale(),
@@ -62,6 +61,18 @@ fun FocusableCard(
 
     CompactCard(
         interactionSource = interactionSource,
+        scale = scale,
+        shape = shape,
+        colors = colors,
+        border = border,
+        glow = CardDefaults.glow(
+            glow = Glow.None,
+            focusedGlow = Glow(
+                elevationColor = Color.Green,
+                elevation = 10.dp
+            ),
+            pressedGlow = Glow.None
+        ),
         modifier = modifier
             .onFocusChanged { state ->
                 focusState.value = state
@@ -98,15 +109,6 @@ fun FocusableCard(
         onClick = {
             onClick(movie)
         },
-        glow = CardDefaults.glow(
-            glow = Glow.None,
-            focusedGlow = Glow(
-                elevationColor = Color.Green,
-                elevation = 10.dp
-            ),
-            pressedGlow = Glow.None
-        ),
-        scale = scale,
     )
 
 }

+ 5 - 7
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Header.kt

@@ -65,7 +65,8 @@ fun Header(
             title = title ?: R.string.app_name,
             fontWeight = fontWeight,
             fontSize = fontSize,
-            color = color
+            color = color,
+            onClick = onTitleClick,
         )
 
         Clock(
@@ -78,6 +79,7 @@ fun Header(
             contentPadding = contentPadding,
             timeTextColor = color,
             dateTextColor = color,
+            onClick = onClockClick,
         )
 
         Badge(
@@ -95,9 +97,7 @@ fun Header(
             borderSize = 1.dp,
             textSize = 20.sp,
             textColor = Color.White,
-            onClick = {
-                onMessageBadgeClick()
-            }
+            onClick = onMessageBadgeClick,
         )
 
         UserPic(
@@ -112,9 +112,7 @@ fun Header(
             borderColor = Color.White,
             contentPadding = contentPadding,
             borderSize = 1.dp,
-            onClick = {
-                onUserPicClick()
-            }
+            onClick = onUserPicClick,
         )
 
     }

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

@@ -8,67 +8,51 @@
 
 package org.mjdev.tvapp.base.ui.components.complex
 
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import org.mjdev.tvapp.R
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 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
+import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
+import org.mjdev.tvapp.base.ui.components.navigation.EmptyScreen
+import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.ui.components.navigation.Navigation
 
-@Suppress("TrailingComma", "UNUSED_ANONYMOUS_PARAMETER", "unused")
+@Suppress("TrailingComma", "unused")
 @TvPreview
 @Composable
 fun ScreenView(
-    navController: NavHostControllerEx? = null,
-    navigationBackgroundColor:Color = Color(0xff202020),
-    actions: @Composable RowScope.() -> Unit = {},
-    title: Any? = R.string.app_name,
-    menuItems: List<MenuItem> = listOf(),
-    content: @Composable (
-        state: ScreenState,
-    ) -> Unit = { state ->
-        Screen()
-    }
+    navController: NavHostControllerEx = rememberNavControllerEx(),
+    backgroundColor: Color = Color.DarkGray,
+    roundCornerSize: Dp = 16.dp,
+    shape: Shape = RoundedCornerShape(roundCornerSize),
+    content: @Composable () -> Unit = { EmptyScreen() }
 ) {
-
-    val isEdit = isEditMode()
-    val screenState = rememberScreenState(title)
-    val errorState = remember { screenState.errorState }
-
     Box(
-        modifier = Modifier.fillMaxSize(),
+        modifier = Modifier
+            .fillMaxSize()
+            .background(backgroundColor, shape),
         contentAlignment = Alignment.BottomCenter,
     ) {
 
-        Navigation(
-            navController = navController,
-            backgroundColor = navigationBackgroundColor,
-            items = menuItems,
-            content = {
-                Column(
-                    modifier = Modifier.fillMaxSize()
-                ) {
-                    content(screenState)
+        if (navController.menuState.value) {
+            Navigation(
+                navController = navController,
+                backgroundColor = backgroundColor,
+                content = {
+                    content()
                 }
-            }
-        )
-
-        if (isEdit || (errorState.value != null)) {
-            ErrorMessage(errorState.value)
+            )
+        } else {
+            content()
         }
 
     }
-
 }

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

@@ -89,6 +89,9 @@ fun <T> Tabs(
                 onFocus = {
                     selectedTabIndex.value = index
                 },
+                onClick = {
+                    onItemClick(selectedTabIndex.value)
+                },
                 interactionSource = interactionSource
             ) {
 

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

@@ -30,7 +30,7 @@ fun UserPic(
     shape: Shape = CircleShape,
     src: Any? = R.drawable.milanj,
     backGroundColor: Color = MaterialTheme.colorScheme.secondary,
-    borderColor: Color = Color.White,
+    borderColor: Color = Color.Transparent,
     borderSize: Dp = 2.dp,
     contentPadding: Dp = 2.dp,
     onClick: () -> Unit = {}

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

@@ -49,7 +49,7 @@ fun IconAny(
 
     when (src) {
         null -> Icon(
-            ColorDrawable(0).asImageBitmap(),
+            ColorDrawable(0).asImageBitmap(width, height),
             contentDescription,
             modifier,
             tint
@@ -121,7 +121,7 @@ fun IconAny(
             tint
         )
 
-        else -> throw (RuntimeException("Unknown image format."))
+        else -> throw (RuntimeException("Unknown image format $src."))
     }
 
 }

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

@@ -31,8 +31,8 @@ import org.mjdev.tvapp.R
 fun CircleImage(
     modifier: Modifier = Modifier,
     backGroundColor: Color = Color.Black,
-    borderColor: Color = Color.White,
-    borderSize: Dp = 2.dp,
+    borderColor: Color = Color.Transparent,
+    borderSize: Dp = 0.dp,
     contentDescription: String = "",
     contentPadding: Dp = 2.dp,
     src: Any? = R.drawable.milanj

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

@@ -173,7 +173,7 @@ fun ImageAny(
             colorFilter
         )
 
-//        else -> throw (RuntimeException("Unknown image format."))
+        else -> throw (RuntimeException("Unknown image format $src."))
 
     }
 

+ 5 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/media/IMediaPlayer.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/IMediaPlayer.kt

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

+ 6 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerContainer.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerContainer.kt

@@ -1,14 +1,14 @@
 /*
  * Copyright (c) Milan Jurkulák 2023. 
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
 @file:Suppress("unused")
 
-package org.mjdev.tvapp.base.media
+package org.mjdev.tvapp.base.ui.components.media
 
 import android.net.Uri
 import androidx.compose.foundation.layout.Box
@@ -16,7 +16,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
 import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.media.MediaPlayerState.Companion.rememberMediaPlayerState
+import org.mjdev.tvapp.base.ui.components.media.MediaPlayerState.Companion.rememberMediaPlayerState
 
 @TvPreview
 @Composable

+ 5 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerContainerDefaults.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerContainerDefaults.kt

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

+ 5 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerImpl.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerImpl.kt

@@ -1,12 +1,12 @@
 /*
  * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.media
+package org.mjdev.tvapp.base.ui.components.media
 
 import android.net.Uri
 import androidx.compose.foundation.background

+ 5 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/media/MediaPlayerState.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/MediaPlayerState.kt

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

+ 5 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/media/NativePlayerImpl.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/NativePlayerImpl.kt

@@ -1,12 +1,12 @@
 /*
  * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.media
+package org.mjdev.tvapp.base.ui.components.media
 
 import android.media.MediaPlayer
 import android.net.Uri

+ 1 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/media/PlayerControls.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/media/PlayerControls.kt

@@ -6,7 +6,7 @@
  * w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.media
+package org.mjdev.tvapp.base.ui.components.media
 
 import androidx.compose.runtime.Composable
 import org.mjdev.tvapp.base.annotations.TvPreview

+ 5 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/EmptyScreen.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/EmptyScreen.kt

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

+ 85 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/MenuItem.kt

@@ -0,0 +1,85 @@
+/*
+ * 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.navigation
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ExitToApp
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Settings
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.ui.components.page.Page
+
+@Suppress("unused")
+data class MenuItem(
+
+    var menuText: Any? = null,
+    var menuIcon: Any? = null,
+
+    var menuGravity: Gravity = Gravity.Center,
+
+    var menuAction: (() -> Unit)? = null,
+    var menuRoute: String? = null,
+    var menuPage: Page? = null,
+
+    var enabled: Boolean = true,
+
+    ) {
+
+    val isPage: Boolean get() = (menuPage != null)
+
+    val isRoute: Boolean get() = (menuRoute != null)
+
+    val isAction: Boolean get() = (menuAction != null)
+
+    val isEnabled: Boolean get() = (((menuText != null) || (menuIcon != null)) && enabled)
+
+    fun gravity(gravity: Gravity): MenuItem = apply {
+        menuGravity = gravity
+    }
+
+    fun icon(icon: Any?): MenuItem = apply {
+        menuIcon = icon
+    }
+
+    fun text(text: Any?): MenuItem = apply {
+        menuText = text
+    }
+
+    fun action(action: () -> Unit): MenuItem = apply {
+        menuAction = action
+    }
+
+    enum class Gravity {
+        Top,
+        Bottom,
+        Center
+    }
+
+    override fun toString(): String = "Menu [ $menuText ] action:$isAction, page:$isPage, route:$isRoute"
+
+    companion object {
+
+        val MENU_ITEM_SEARCH = MenuItem(
+            menuText = R.string.menu_title_search,
+            menuIcon = Icons.Default.Search,
+        )
+
+        val MENU_ITEM_SETTINGS = MenuItem(
+            menuText = R.string.menu_title_settings,
+            menuIcon = Icons.Default.Settings,
+        )
+
+        val MENU_ITEM_EXIT = MenuItem(
+            menuText = R.string.menu_title_exit,
+            menuIcon = Icons.Default.ExitToApp,
+        )
+
+    }
+
+}

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

+ 157 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavDrawerContent.kt

@@ -0,0 +1,157 @@
+/*
+ * 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.navigation
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.tv.material3.DrawerState
+import androidx.tv.material3.DrawerValue
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
+import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.open
+
+@SuppressLint("AutoboxingStateValueProperty")
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Preview
+@Composable
+fun NavDrawerContent(
+    navController: NavHostControllerEx = rememberNavControllerEx(),
+    navigationState: NavigationState = rememberNavigationState(
+        navController = navController,
+        drawerState = DrawerState(DrawerValue.Open)
+    ),
+    backgroundColor: Color = Color(0xff202020),
+    onDrawerItemClick: (id: Int) -> Unit = { id ->
+        navController.menuItem(id).let { menuItem ->
+            if (menuItem.isAction) {
+                menuItem.menuAction?.invoke()
+            }
+            if (menuItem.isRoute) {
+                navController.open(menuItem.menuRoute)
+            }
+            if (menuItem.isPage) {
+                navController.onMenuItemClick(menuItem)
+            }
+        }
+    },
+    onDrawerItemFocus: (id: Int) -> Unit = { id ->
+        navController.menuItem(id).let { menuItem ->
+            if (menuItem.isPage) {
+                navController.onMenuItemClick(menuItem)
+            }
+        }
+    },
+) {
+
+    val focused = remember { mutableIntStateOf(-1) }
+
+    val topItems = navController.menuItems.filter { mi ->
+        mi.menuGravity == MenuItem.Gravity.Top
+    }
+
+    val bottomItems = navController.menuItems.filter { mi ->
+        mi.menuGravity == MenuItem.Gravity.Bottom
+    }
+
+    val centerItems = navController.menuItems.filter { mi ->
+        mi.menuGravity == MenuItem.Gravity.Center
+    }
+
+    val itemId: (menuItem: MenuItem) -> Int = { menuItem ->
+        navController.indexOfMenuItem(menuItem)
+    }
+
+    Column(
+        modifier = Modifier
+            .fillMaxHeight()
+            .background(backgroundColor),
+        verticalArrangement = Arrangement.Bottom,
+        horizontalAlignment = Alignment.Start,
+    ) {
+        bottomItems.forEach { menuItem ->
+            if (menuItem.isEnabled) {
+                NavigationRow(
+                    id = itemId(menuItem),
+                    drawerState = navigationState.drawerState,
+                    icon = menuItem.menuIcon,
+                    text = menuItem.menuText,
+                    onFocus = { id ->
+                        navigationState.openDrawer()
+                        focused.value = id
+                        onDrawerItemFocus(id)
+                    },
+                    onClick = { id ->
+                        onDrawerItemClick(id)
+                    }
+                )
+            }
+        }
+    }
+    Column(
+        modifier = Modifier
+            .fillMaxHeight(),
+        verticalArrangement = Arrangement.Top,
+        horizontalAlignment = Alignment.Start,
+    ) {
+        topItems.forEach { menuItem ->
+            if (menuItem.isEnabled) {
+                NavigationRow(
+                    id = itemId(menuItem),
+                    drawerState = navigationState.drawerState,
+                    icon = menuItem.menuIcon,
+                    text = menuItem.menuText,
+                    onFocus = { id ->
+                        navigationState.openDrawer()
+                        focused.value = id
+                        onDrawerItemFocus(id)
+                    },
+                    onClick = { id ->
+                        onDrawerItemClick(id)
+                    }
+                )
+            }
+        }
+    }
+    Column(
+        modifier = Modifier
+            .fillMaxHeight(),
+        verticalArrangement = Arrangement.Center,
+        horizontalAlignment = Alignment.Start,
+    ) {
+        centerItems.forEach { menuItem ->
+            if (menuItem.isEnabled) {
+                NavigationRow(
+                    id = itemId(menuItem),
+                    drawerState = navigationState.drawerState,
+                    text = menuItem.menuText,
+                    icon = menuItem.menuIcon,
+                    onFocus = { id ->
+                        navigationState.openDrawer()
+                        focused.value = id
+                        onDrawerItemFocus(id)
+                    },
+                    onClick = { id ->
+                        onDrawerItemClick(id)
+                    }
+                )
+            }
+        }
+    }
+
+}

+ 6 - 7
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavGraphBuilderEx.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavGraphBuilderEx.kt

@@ -1,17 +1,17 @@
 /*
  * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.navigation
+package org.mjdev.tvapp.base.ui.components.navigation
 
 import androidx.navigation.NavGraph
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.NavigatorProvider
-import org.mjdev.tvapp.base.navigation.EmptyScreen.Companion.ROUTE_NONE
+import org.mjdev.tvapp.base.ui.components.navigation.EmptyScreen.Companion.ROUTE_NONE
 
 class NavGraphBuilderEx(
     provider: NavigatorProvider,
@@ -22,7 +22,6 @@ class NavGraphBuilderEx(
 
     var splashDestinationRoute: String? = startRoute
     var homeDestinationRoute: String = startRoute ?: ROUTE_NONE
-    var menuItems: MutableList<MenuItem> = mutableListOf()
 
     override fun build(): NavGraph {
         val navGraph = super.build()

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

@@ -0,0 +1,101 @@
+/*
+ * 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.navigation
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.navigation.NavHostController
+import org.mjdev.tvapp.base.extensions.ListExt.addUnique
+
+// todo apply lifecycle
+@Suppress("unused", "MemberVisibilityCanBePrivate")
+class NavHostControllerEx(
+    context: Context
+) : NavHostController(
+    context
+), LifecycleOwner, DefaultLifecycleObserver {
+
+    val menuState: MutableState<Boolean> = mutableStateOf(true)
+
+    val menuItems: SnapshotStateList<MenuItem> = mutableStateListOf()
+
+    val menuClickListeners: MutableList<MenuItemClickListener> = mutableListOf()
+
+    override val lifecycle: Lifecycle by lazy { LifecycleRegistry(this) }
+
+    init {
+        lifecycle.addObserver(this)
+    }
+
+    override fun onDestroy(owner: LifecycleOwner) {
+        lifecycle.removeObserver(this)
+        clearClickListeners()
+        clearMenuItems()
+    }
+
+    fun addMenuItem(vararg item: MenuItem) {
+        item.forEach { mi -> addUniqueMenuItem(mi) }
+    }
+
+    fun removeMenuItem(vararg item: MenuItem) {
+        menuItems.removeAll(item.toSet())
+    }
+
+    fun clearMenuItems() {
+        menuItems.clear()
+    }
+
+    fun addMenuClickListener(vararg listener: MenuItemClickListener) {
+        menuClickListeners.addAll(listener)
+    }
+
+    fun removeMenuClickListener(vararg listener: MenuItemClickListener) {
+        menuClickListeners.removeAll(listener.toSet())
+    }
+
+    fun clearClickListeners() {
+        menuClickListeners.clear()
+    }
+
+    fun onMenuItemClick(item: MenuItem) {
+        menuClickListeners.forEach { l ->
+            l.onMenuItemClick(item)
+        }
+    }
+
+    fun indexOfMenuItem(item: MenuItem): Int {
+        return menuItems.indexOf(item)
+    }
+
+    fun addUniqueMenuItem(item: MenuItem) {
+        menuItems.addUnique(item)
+    }
+
+    fun menuItem(id: Int): MenuItem {
+        return menuItems[id]
+    }
+
+    @SuppressLint("ComposableNaming")
+    @Composable
+    fun forEachMenuItem(block: @Composable (index: Int, menuItem: MenuItem) -> Unit) {
+        menuItems.forEachIndexed { idx, mi ->
+            block.invoke(idx, mi)
+        }
+    }
+
+}

+ 6 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostEx.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavHostEx.kt

@@ -1,12 +1,12 @@
 /*
  * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.navigation
+package org.mjdev.tvapp.base.ui.components.navigation
 
 import androidx.compose.animation.AnimatedContentTransitionScope
 import androidx.compose.animation.EnterTransition
@@ -23,7 +23,7 @@ import androidx.navigation.compose.NavHost
 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
+import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.screen
 
 @TvPreview
 @Composable

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

@@ -11,19 +11,12 @@ 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.Arrangement
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Search
-import androidx.compose.material.icons.outlined.Settings
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
 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.tv.material3.DrawerValue
@@ -32,158 +25,53 @@ import androidx.tv.material3.NavigationDrawer
 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
+import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
 
-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: NavHostControllerEx? = null,
-    items: List<MenuItem> = listOf(),
-    backgroundColor: Color = Color.DarkGray,
-    roundSize: Dp = 8.dp,
+    navController: NavHostControllerEx = rememberNavControllerEx(),
+    navigationState: NavigationState = rememberNavigationState(
+        navController = navController,
+        drawerState = rememberDrawerState(
+            initialValue = DrawerValue.Open
+        )
+    ),
+    backgroundColor: Color = Color(0xff202020),
+    roundCornerSize: Dp = 8.dp,
     borderSize: Dp = 0.dp,
-    borderColor: Color = Color.White,
-    showSearch: Boolean = true,
-    showSettings: Boolean = true,
-    searchArrangement: Arrangement.Vertical = Arrangement.Top,
-    settingsArrangement: Arrangement.Vertical = Arrangement.Bottom,
-    onSettingsItemFocused: () -> Unit = {},
-    onSearchItemFocused: () -> Unit = {},
-    onSettingsItemClicked: () -> Unit = {},
-    onSearchItemClicked: () -> Unit = {},
-    onDrawerItemClick: (id: Int) -> Unit = {},
-    onDrawerItemFocused: (id: Int) -> Unit = {},
-    content: @Composable () -> Unit = {}
+    borderColor: Color = Color.Transparent,
+    shape: Shape = RoundedCornerShape(roundCornerSize),
+    content: @Composable () -> Unit = {},
+    menuItems: List<MenuItem> = listOf()
 ) {
 
     val isEdit = isEditMode()
-    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)
+    if (isEdit) navigationState.openDrawer()
+    navController.addMenuItem(*menuItems.toTypedArray())
+
+    if (navController.menuState.value) {
+        NavigationDrawer(
+            modifier = modifier
+                .fillMaxHeight()
+                .background(backgroundColor, shape)
+                .border(borderSize, borderColor, shape),
+            content = content,
+            drawerState = navigationState.drawerState,
+            drawerContent = { state ->
+                NavDrawerContent(
+                    backgroundColor = backgroundColor,
+                    navController = navController,
+                    navigationState = navigationState,
+                )
+                navigationState.drawerState.setValue(state)
             }
-        }
+        )
+    } else {
+        content()
     }
 
-    NavigationDrawer(
-        modifier = modifier
-            .fillMaxHeight()
-            .background(backgroundColor)
-            .apply {
-                if (borderSize > 0.dp) {
-                    border(borderSize, borderColor, RoundedCornerShape(roundSize))
-                }
-            },
-        content = content,
-        drawerState = drawerState,
-        drawerContent = { state ->
-
-            focused.value.apply {
-
-                if (showSettings) {
-                    Column(
-                        modifier = Modifier.fillMaxHeight(),
-                        verticalArrangement = settingsArrangement,
-                        horizontalAlignment = Alignment.Start,
-                    ) {
-
-                        NavigationRow(
-                            id = SETTINGS_ITEM,
-                            drawerValue = state,
-                            icon = Icons.Outlined.Settings,
-                            text = "Settings",
-                            onFocus = { id ->
-                                drawerState.setValue(DrawerValue.Open)
-                                focused.value = id
-                                _onDrawerItemFocus(id)
-                            },
-                            onClick = { id ->
-                                _onDrawerItemClick(id)
-                            }
-                        )
-
-                    }
-                }
-
-                if (showSearch) {
-                    Column(
-                        modifier = Modifier
-                            .fillMaxHeight(),
-                        verticalArrangement = searchArrangement,
-                        horizontalAlignment = Alignment.Start,
-                    ) {
-
-                        NavigationRow(
-                            id = SEARCH_ITEM,
-                            drawerValue = state,
-                            icon = Icons.Outlined.Search,
-                            text = "Search",
-                            onFocus = { id ->
-                                drawerState.setValue(DrawerValue.Open)
-                                focused.value = id
-                                _onDrawerItemFocus(id)
-                            },
-                            onClick = { id ->
-                                _onDrawerItemClick(id)
-                            }
-                        )
-
-                    }
-                }
-
-                Column(
-                    modifier = Modifier
-                        .fillMaxHeight(),
-                    verticalArrangement = Arrangement.Center,
-                    horizontalAlignment = Alignment.Start,
-                ) {
-
-                    items.forEachIndexed { idx, menuItem ->
-                        NavigationRow(
-                            id = idx,
-                            drawerValue = state,
-                            text = menuItem.menuText,
-                            icon = menuItem.menuIcon,
-                            onFocus = { id ->
-                                drawerState.setValue(DrawerValue.Open)
-                                focused.value = id
-                                _onDrawerItemFocus(id)
-                            },
-                            onClick = { id ->
-                                _onDrawerItemClick(id)
-                            }
-                        )
-                    }
-
-                }
-
-            }
-        }
-    )
-
 }

+ 13 - 13
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavigationRow.kt

@@ -18,6 +18,8 @@ import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AccountCircle
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -25,8 +27,10 @@ import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.rememberDrawerState
 import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
 import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.icon.IconAny
@@ -38,8 +42,8 @@ import org.mjdev.tvapp.base.ui.components.text.TextAny
 fun NavigationRow(
     modifier: Modifier = Modifier,
     id: Int = -1,
-    drawerValue: DrawerValue = DrawerValue.Open,
-    text: Any? = "test",
+    drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Open),
+    text: Any? = "menu item 1",
     icon: Any? = Icons.Default.AccountCircle,
     textColor: Color = Color.White,
     iconColor: Color = Color.White,
@@ -50,13 +54,15 @@ fun NavigationRow(
     onFocus: (id: Int) -> Unit = {},
     onClick: (id: Int) -> Unit = {},
 ) {
+
     val isEdit = isEditMode()
+    val focused = remember { mutableStateOf(false) }
 
     FocusableBox(
-        modifier = Modifier
+        modifier = modifier
             .padding(contentPadding)
             .background(
-                if (isEdit) Color.DarkGray else Color.Transparent
+                if (isEdit || focused.value) focusedColor else unFocusedColor
             ),
         focusedColor = focusedColor,
         unFocusedColor = unFocusedColor,
@@ -67,26 +73,22 @@ fun NavigationRow(
             onClick(id)
         },
     ) {
-
         Row(
             horizontalArrangement = Arrangement.Start,
             verticalAlignment = Alignment.CenterVertically,
         ) {
-
             Box(
                 modifier = Modifier.padding(4.dp)
             ) {
-
                 IconAny(
                     src = icon,
                     contentDescription = null,
                     tint = iconColor
                 )
-
             }
-
-            AnimatedVisibility(visible = (drawerValue == DrawerValue.Open)) {
-
+            AnimatedVisibility(
+                visible = (drawerState.currentValue == DrawerValue.Open)
+            ) {
                 TextAny(
                     text = text,
                     softWrap = false,
@@ -98,9 +100,7 @@ fun NavigationRow(
                 )
 
             }
-
         }
-
     }
 
 }

+ 58 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavigationState.kt

@@ -0,0 +1,58 @@
+/*
+ * 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.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.tv.material3.DrawerState
+import androidx.tv.material3.DrawerValue
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.rememberDrawerState
+
+@Suppress("unused")
+@OptIn(ExperimentalTvMaterial3Api::class)
+class NavigationState constructor(
+    val navController: NavHostControllerEx,
+    val drawerState: DrawerState = DrawerState(DrawerValue.Open),
+) {
+
+    fun openDrawer() {
+        drawerState.open()
+    }
+
+    fun closeDrawer() {
+        drawerState.close()
+    }
+
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+fun DrawerState.open() {
+    if (currentValue == DrawerValue.Closed) {
+        setValue(DrawerValue.Open)
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+fun DrawerState.close() {
+    if (currentValue == DrawerValue.Open) {
+        setValue(DrawerValue.Closed)
+    }
+}
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun rememberNavigationState(
+    navController: NavHostControllerEx,
+    drawerState: DrawerState = rememberDrawerState(
+        initialValue = DrawerValue.Open
+    ),
+) = NavigationState(
+    navController = navController,
+    drawerState = drawerState,
+)

+ 46 - 64
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/Screen.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Screen.kt

@@ -1,14 +1,14 @@
 /*
  * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
 @file:Suppress("MemberVisibilityCanBePrivate")
 
-package org.mjdev.tvapp.base.navigation
+package org.mjdev.tvapp.base.ui.components.navigation
 
 import android.os.Bundle
 import androidx.annotation.CallSuper
@@ -16,9 +16,6 @@ import androidx.annotation.MainThread
 import androidx.compose.animation.AnimatedContentTransitionScope
 import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -43,17 +40,11 @@ import androidx.navigation.NavHostController
 import androidx.navigation.compose.composable
 import androidx.tv.material3.Text
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.extensions.AnimExt.FadeIn
+import org.mjdev.tvapp.base.extensions.AnimExt.FadeOut
+import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
 import kotlin.reflect.full.createInstance
 
-/**
- * Screen class.
- *
- * This is base of all screens in application and auto creation of route and nav graph.
- * This object represents one screen at time.
- * Can contain menu that is exported to navigation drawer.
- *
- * @constructor Create empty constructor for screen
- */
 @Suppress("unused", "LeakingThis")
 open class Screen : NavController.OnDestinationChangedListener {
 
@@ -69,45 +60,59 @@ open class Screen : NavController.OnDestinationChangedListener {
             routeImpl
         }
 
-    open val titleResId: Int = -1
-
     open val args: List<NamedNavArgument> = emptyList()
 
-    open val menuTitleResId: Int = -1
+    open val title: Any? = null
+
+    open val menuTitle: Any? = null
 
     open val menuIcon: ImageVector? = null
 
-    open val showOnce = false
+    open val backgroundColor: Color = Color.DarkGray
 
-    open val backgroundColor :Color = Color.DarkGray
+    open val roundRadius: Dp = 0.dp
 
-    open val roundRadius : Dp = 0.dp
+    open val background: Shape = RoundedCornerShape(roundRadius)
 
-    open val background : Shape = RoundedCornerShape(roundRadius)
+    open val showOnce = false
+
+    open val immersive: Boolean = false
 
     val menuItem
-        get() = if (menuTitleResId >= 0) MenuItem(menuTitleResId, menuIcon, completeRoute)
-        else null
+        get() = if (menuTitle != null) MenuItem(
+            menuText = menuTitle,
+            menuIcon = menuIcon,
+            menuRoute = completeRoute
+        ) else null
 
     @TvPreview
     @Composable
     @CallSuper
     open fun Compose() {
-        Compose(null, null, emptyList(), mapOf())
+
+        val navController = rememberNavControllerEx()
+
+        Compose(
+            navController,
+            null,
+            mapOf()
+        )
+
     }
 
     @Composable
     open fun Compose(
-        navController: NavHostControllerEx?,
+        navController: NavHostControllerEx,
         backStackEntry: NavBackStackEntry?,
-        menuItems: List<MenuItem>,
         args: Map<String, Any?>
     ) {
 
         if (showOnce) {
-            navController?.addOnDestinationChangedListener(this)
+            navController.addOnDestinationChangedListener(this)
         }
 
+        navController.menuState.value = !immersive
+
         Column(
             modifier = Modifier
                 .fillMaxSize()
@@ -115,14 +120,12 @@ open class Screen : NavController.OnDestinationChangedListener {
             verticalArrangement = Arrangement.Center,
             horizontalAlignment = Alignment.CenterHorizontally
         ) {
-
             Text(
                 text = "Empty Screen",
                 fontSize = 64.sp,
                 fontWeight = FontWeight.Bold,
                 color = Color.White
             )
-
         }
 
     }
@@ -139,9 +142,6 @@ open class Screen : NavController.OnDestinationChangedListener {
 
     companion object {
 
-        val SlideInVertically = slideInVertically(animationSpec = tween(1000))
-        val SlideOutVertically = slideOutVertically(animationSpec = tween(1000))
-
         @Suppress("UNCHECKED_CAST", "DEPRECATION")
         private fun <T> NavBackStackEntry.arg(
             argId: String,
@@ -149,34 +149,20 @@ open class Screen : NavController.OnDestinationChangedListener {
         ): T = (arguments?.get(argId) as? T?) ?: defaultValue
 
         @MainThread
-        inline fun <reified T : Screen> NavHostController.open(
-            vararg values: Any?
+        fun NavHostController.open(
+            route: String?
         ) {
-            val instance = T::class.createInstance()
-            instance.completeRoute.let { r ->
-                var routeImpl = r
-                instance.args.forEachIndexed { idx, arg ->
-                    routeImpl = routeImpl.replace(
-                        "{${arg.name}}",
-                        (values[idx] ?: "").toString()
-                    )
-                }
-                routeImpl
-            }.also { finalRoute ->
+            if (route != null) {
                 val currentRoute = currentRoute
-                val equals = currentRoute?.equals(finalRoute)
+                val equals = currentRoute?.equals(route)
                 if (equals != true) {
-//                    try {
-                        navigate(finalRoute)
-//                    } catch (e: Exception) {
-//                        Timber.e(e)
-//                    }
+                    navigate(route)
                 }
             }
         }
 
         @MainThread
-        inline fun <reified T : Screen> NavHostController.openClear(
+        inline fun <reified T : Screen> NavHostController.open(
             vararg values: Any?
         ) {
             val instance = T::class.createInstance()
@@ -193,11 +179,7 @@ open class Screen : NavController.OnDestinationChangedListener {
                 val currentRoute = currentRoute
                 val equals = currentRoute?.equals(finalRoute)
                 if (equals != true) {
-//                    try {
-                        navigate(finalRoute)
-//                    } catch (e: Exception) {
-//                        Timber.e(e)
-//                    }
+                    navigate(finalRoute)
                 }
             }
         }
@@ -212,11 +194,11 @@ open class Screen : NavController.OnDestinationChangedListener {
             deepLinks: List<NavDeepLink> = emptyList(),
             enterTransition: (@JvmSuppressWildcards
             AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? = {
-                SlideInVertically
+                FadeIn
             },
             exitTransition: (@JvmSuppressWildcards
             AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? = {
-                SlideOutVertically
+                FadeOut
             },
             popEnterTransition: (@JvmSuppressWildcards
             AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
@@ -232,7 +214,7 @@ open class Screen : NavController.OnDestinationChangedListener {
                 homeDestinationRoute = route.completeRoute
             }
             route.menuItem?.also { menuItem ->
-                menuItems.add(menuItem)
+                navHostController.addMenuItem(menuItem)
             }
             composable(
                 route = route.completeRoute,
@@ -248,7 +230,7 @@ open class Screen : NavController.OnDestinationChangedListener {
                         put(arg.name, be.arg(arg.name, null))
                     }
                 }
-                route.Compose(navHostController, be, menuItems, rArgs)
+                route.Compose(navHostController, be, rArgs)
             }
         }
 

+ 45 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/ScreenWithPages.kt

@@ -0,0 +1,45 @@
+/*
+ * 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.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavBackStackEntry
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.page.Pager
+import org.mjdev.tvapp.base.ui.components.page.PagerScope
+import org.mjdev.tvapp.base.ui.components.complex.ScreenView
+
+open class ScreenWithPages : Screen() {
+
+    open val startPageIndex: Int = 0
+
+    open val pages: PagerScope.() -> Unit = {}
+
+    @TvPreview
+    @Composable
+    override fun Compose() = super.Compose()
+
+    @Composable
+    override fun Compose(
+        navController: NavHostControllerEx,
+        backStackEntry: NavBackStackEntry?,
+        args: Map<String, Any?>
+    ) {
+        ScreenView(
+            navController = navController,
+        ) {
+            Pager(
+                navController = navController,
+                startIndex = startPageIndex,
+                pages = pages
+            )
+        }
+    }
+
+}

+ 25 - 46
app/src/main/kotlin/org/mjdev/tvapp/base/page/Page.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Page.kt

@@ -1,20 +1,19 @@
 /*
  * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.page
+package org.mjdev.tvapp.base.ui.components.page
 
 import android.annotation.SuppressLint
-import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.annotation.CallSuper
 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
@@ -25,37 +24,30 @@ 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.tv.material3.Text
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
-import org.mjdev.tvapp.base.navigation.NavHostControllerEx
-import org.mjdev.tvapp.base.state.ScreenState
+import org.mjdev.tvapp.base.ui.components.navigation.MenuItem
+import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 @Suppress("LeakingThis")
 open class Page {
 
     var navController: NavHostControllerEx? = null
-    var screenState: ScreenState? = null
-
-    @OptIn(ExperimentalFoundationApi::class)
-    lateinit var pagerState: PagerState
 
     open val title: Any? = null
     open val icon: Any? = null
-
+    open val menuGravity = MenuItem.Gravity.Center
     open val backgroundColor: Color = Color.DarkGray
-
     open val roundRadius: Dp = 0.dp
-
     open val background: Shape = RoundedCornerShape(roundRadius)
 
     @SuppressLint("ComposableNaming")
     @TvPreview
     @Composable
+    @CallSuper
     fun content() {
 
-        screenState?.titleState?.value = title
-
         Column(
             Modifier
                 .fillMaxSize()
@@ -74,39 +66,26 @@ open class Page {
     @Composable
     open fun Content() {
 
-        EMPTY_PAGE
+        Column(
+            modifier = Modifier.fillMaxSize(),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            TextAny(
+                text = title ?: "Empty Page",
+                fontSize = 64.sp,
+                fontWeight = FontWeight.Bold,
+                color = Color.White,
+            )
+        }
 
     }
 
-    override fun toString(): String = title.toString()
+    override fun toString(): String = this::class.simpleName ?: "Page[]"
 
     companion object {
 
-        val EMPTY_PAGE: Page
-            get() = object : Page() {
-
-                @Composable
-                override fun Content() {
-
-                    Column(
-                        modifier = Modifier.fillMaxSize(),
-                        verticalArrangement = Arrangement.Center,
-                        horizontalAlignment = Alignment.CenterHorizontally,
-                    ) {
-
-                        Text(
-
-                            text = "Empty Screen",
-                            fontSize = 64.sp,
-                            fontWeight = FontWeight.Bold,
-                            color = Color.White,
-                        )
-
-                    }
-
-                }
-
-            }
+        val EMPTY_PAGE = Page()
 
     }
 

+ 34 - 36
app/src/main/kotlin/org/mjdev/tvapp/base/page/Pager.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Pager.kt

@@ -1,12 +1,12 @@
 /*
  * Copyright (c) Milan Jurkulák 2023.
- * Contact:
- * e: mimoccc@gmail.com
- * e: mj@mjdev.org
- * w: https://mjdev.org
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.page
+package org.mjdev.tvapp.base.ui.components.page
 
 import android.annotation.SuppressLint
 import androidx.compose.foundation.ExperimentalFoundationApi
@@ -27,49 +27,44 @@ import androidx.compose.ui.unit.dp
 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
+import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
+import org.mjdev.tvapp.base.ui.components.navigation.MenuItemClickListener
+import org.mjdev.tvapp.base.ui.components.navigation.MenuItem
+import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.ui.components.page.Page.Companion.EMPTY_PAGE
 
 @OptIn(ExperimentalFoundationApi::class)
 @SuppressLint("AutoboxingStateValueProperty")
 @TvPreview
 @Composable
 fun Pager(
-    navController: NavHostControllerEx? = null,
-    screenState: ScreenState? = null,
-    menuItems: MutableList<MenuItem> = mutableListOf(),
+    navController: NavHostControllerEx = rememberNavControllerEx(),
     startIndex: Int = 0,
     roundCornerSize: Dp = 0.dp,
     backGroundColor: Color = Color.Transparent,
     backGroundShape: Shape = RoundedCornerShape(roundCornerSize),
-    pages: @Composable PagerScope.() -> Unit = {}
+    pages: PagerScope.() -> Unit = {}
 ) {
 
-    val coroutineScope = rememberCoroutineScope()
-
     val isEdit = isEditMode()
 
-    val pagerScope = PagerScope(navController, screenState, menuItems).apply {
-        pages.invoke(this)
-    }
-
+    val currentPage = remember { mutableStateOf<Page?>(null) }
+    val coroutineScope = rememberCoroutineScope()
+    val pagerScope = rememberPagerScope(navController, pages)
     val pagerState = rememberPagerState(
         pageCount = { pagerScope.size },
         initialPage = startIndex,
     )
 
-    val lastItem = remember { mutableStateOf<MenuItem?>(null) }
-
-    val listener = object : MenuItemClickListener {
-        override fun onMenuItemClick(item: MenuItem) {
-            if (lastItem.value != item) {
-                lastItem.value = item
-                val itemIndex = menuItems.indexOf(item)
-                if (pagerState.currentPage != itemIndex) {
-                    coroutineScope.launch {
+    val goToPage: (
+        page: Page?
+    ) -> Unit = { page ->
+        coroutineScope.launch {
+            if (page != null) {
+                val itemIndex = pagerScope.indexOfPage(page)
+                if (itemIndex > -1) {
+                    if (currentPage.value != page) {
+                        currentPage.value = page
                         pagerState.animateScrollToPage(itemIndex)
                     }
                 }
@@ -77,7 +72,15 @@ fun Pager(
         }
     }
 
-    navController?.addMenuClickListener(listener)
+    val listener = object : MenuItemClickListener {
+        override fun onMenuItemClick(item: MenuItem) {
+            if (item.isPage) {
+                goToPage(item.menuPage)
+            }
+        }
+    }
+
+    navController.addMenuClickListener(listener)
 
     VerticalPager(
         modifier = Modifier
@@ -91,12 +94,7 @@ fun Pager(
         else if (isEdit) EMPTY_PAGE
         else null
 
-        page?.let { p ->
-            p.navController = navController
-            p.screenState = screenState
-            p.pagerState = pagerState
-            p.content()
-        }
+        page?.content()
 
     }
 

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

@@ -0,0 +1,61 @@
+/*
+ * 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.page
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import org.mjdev.tvapp.base.extensions.ListExt.addUnique
+import org.mjdev.tvapp.base.ui.components.navigation.MenuItem
+import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
+
+class PagerScope(
+    val navController: NavHostControllerEx,
+    pages: PagerScope.() -> Unit = {}
+) : ArrayList<Page>() {
+
+    init {
+        pages.invoke(this)
+    }
+
+    fun page(page: Page) {
+        page.navController = navController
+        addUnique(page)
+        page.let { p ->
+            MenuItem(
+                menuText = p.title,
+                menuIcon = p.icon,
+                menuPage = p,
+                menuGravity = p.menuGravity,
+            )
+        }.also { menuItem ->
+            navController.addUniqueMenuItem(menuItem)
+        }
+    }
+
+    fun indexOfPage(page: Page?): Int {
+        return if (page == null) return -1
+        else {
+            indexOfFirst { p ->
+                p.toString() == page.toString()
+            }
+        }
+    }
+
+}
+
+@Composable
+fun rememberPagerScope(
+    navController: NavHostControllerEx,
+    pages: PagerScope.() -> Unit = {}
+) = remember {
+    PagerScope(
+        navController,
+        pages
+    )
+}

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


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

@@ -0,0 +1,30 @@
+/*
+ * 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.page
+
+import android.annotation.SuppressLint
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.runtime.Composable
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.navigation.MenuItem
+
+open class SearchPage : Page() {
+
+    override val title: Any? = R.string.menu_title_search
+    override val icon: Any? = Icons.Default.Search
+    override val menuGravity = MenuItem.Gravity.Top
+
+    @SuppressLint("ComposableNaming")
+    @TvPreview
+    @Composable
+    override fun Content() = super.Content()
+
+}

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

@@ -0,0 +1,30 @@
+/*
+ * 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.page
+
+import android.annotation.SuppressLint
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.runtime.Composable
+import org.mjdev.tvapp.R
+import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.navigation.MenuItem
+
+open class SettingsPage : Page() {
+
+    override val title: Any? = R.string.menu_title_settings
+    override val icon: Any? = Icons.Default.Settings
+    override val menuGravity = MenuItem.Gravity.Bottom
+
+    @SuppressLint("ComposableNaming")
+    @TvPreview
+    @Composable
+    override fun Content() = super.Content()
+
+}

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

@@ -38,8 +38,8 @@ import androidx.tv.material3.MaterialTheme
 fun CircleText(
     modifier: Modifier = Modifier,
     backGroundColor: Color = MaterialTheme.colorScheme.secondary,
-    borderColor: Color = Color.White,
-    borderSize: Dp = 1.dp,
+    borderColor: Color = Color.Transparent,
+    borderSize: Dp = 0.dp,
     text: String = "0",
     textColor: Color = Color.White,
     textSize: TextUnit = 20.sp,

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

@@ -14,7 +14,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.interfaces.ItemWithVideoUri
-import org.mjdev.tvapp.base.media.MediaPlayerContainer
+import org.mjdev.tvapp.base.ui.components.media.MediaPlayerContainer
 
 @SuppressLint("ModifierParameter")
 @TvPreview

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

@@ -28,7 +28,7 @@ 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.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
@@ -42,13 +42,11 @@ class AboutPage : Page() {
     @TvPreview
     @Composable
     override fun Content() {
-
         Box(
             modifier = Modifier
                 .fillMaxSize()
                 .padding(16.dp)
         ) {
-
             Card(
                 onClick = {},
                 onLongClick = {},
@@ -58,7 +56,6 @@ class AboutPage : Page() {
                     containerColor = Color(0xff242424)
                 ),
             ) {
-
                 Row(
                     modifier = Modifier
                         .fillMaxSize()
@@ -97,9 +94,7 @@ class AboutPage : Page() {
 
                     }
                 )
-
             }
-
         }
 
     }

+ 13 - 21
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

@@ -21,6 +21,7 @@ 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
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -37,9 +38,9 @@ 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.navigation.Screen.Companion.open
+import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.open
 import org.mjdev.tvapp.base.network.NetworkStatus
-import org.mjdev.tvapp.base.page.Page
+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
@@ -60,7 +61,6 @@ class MainPage : Page() {
     @TvPreview
     @Composable
     override fun Content() {
-
         val isEdit = isEditMode()
 
         val viewModel: MainViewModel = appViewModel { context ->
@@ -71,6 +71,8 @@ class MainPage : Page() {
         val featuredMovieList = remember { viewModel.featuredMovieList }.collectAsState()
         val messages = remember { viewModel.messages }.collectAsState()
         val networkState = remember { viewModel.networkInfo.networkStatus }.collectAsState(null)
+        val errorState = remember { mutableStateOf<Throwable?>(null) }
+        val titleState = remember { mutableStateOf<Any?>(R.string.app_name) }
 
         val onItemClick: (movie: Movie?) -> Unit = { movie ->
             if (movie == null) {
@@ -83,7 +85,7 @@ class MainPage : Page() {
         }
 
         viewModel.handleError { error ->
-            screenState?.error(error)
+            errorState.value = error
         }
 
         ScrollableTvLazyRow(
@@ -93,16 +95,12 @@ class MainPage : Page() {
             verticalArrangement = Arrangement.spacedBy(32.dp),
             contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
         ) {
-
             item {
-
                 Header(
-                    title = screenState?.titleState?.value,
+                    title = titleState.value,
                     messagesCount = messages.value.size
                 )
-
             }
-
             if (isEdit || (networkState.value !is NetworkStatus.Connected)) item {
                 ErrorMessage(
                     error = stringResource(R.string.error_no_network).asException(),
@@ -110,14 +108,17 @@ class MainPage : Page() {
                     dismissible = false
                 )
             }
-
-
+            if (isEdit || (errorState.value is Exception)) item {
+                ErrorMessage(
+                    error = errorState.value,
+                    dismissible = false
+                )
+            }
             if (isEdit || categoryList.value.isNotEmpty()) item {
                 Tabs(
                     items = categoryList.value.map { it.name }
                 )
             }
-
             if (isEdit || featuredMovieList.value.isNotEmpty()) item {
                 BigCarousel(
                     modifier = Modifier.touchable(),
@@ -125,9 +126,7 @@ class MainPage : Page() {
                     onItemClicked = onItemClick
                 )
             }
-
             items(categoryList.value) { category ->
-
                 Text(
                     modifier = Modifier
                         .fillMaxWidth()
@@ -136,7 +135,6 @@ class MainPage : Page() {
                     color = Color.White,
                     fontWeight = FontWeight.Bold
                 )
-
                 TvLazyRow(
                     horizontalArrangement = Arrangement.spacedBy(16.dp),
                     modifier = Modifier.height(200.dp),
@@ -147,20 +145,14 @@ class MainPage : Page() {
                         end = 16.dp
                     )
                 ) {
-
                     items(category.movieList) { item ->
-
                         MovieCard(
                             movie = item,
                             onClick = onItemClick
                         )
-
                     }
-
                 }
-
             }
-
         }
 
     }

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

@@ -27,7 +27,7 @@ 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.page.Page
 import org.mjdev.tvapp.base.ui.components.card.CardContent
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 

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

@@ -21,9 +21,8 @@ import org.mjdev.tvapp.R
 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.base.ui.components.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.ui.components.navigation.Screen
 import org.mjdev.tvapp.state.DetailsLoadingState
 import org.mjdev.tvapp.ui.components.Details
 import org.mjdev.tvapp.ui.components.Loading
@@ -33,7 +32,11 @@ class DetailScreen : Screen() {
 
     private val movieId = "movieId"
 
-    override val titleResId = R.string.title_movie_detail
+    override val title = R.string.title_movie_detail
+
+    override val menuIcon: ImageVector get() = Icons.Filled.Info
+
+    override val immersive: Boolean = true
 
     override val args = listOf(
         navArgument(movieId) {
@@ -42,19 +45,14 @@ class DetailScreen : Screen() {
         }
     )
 
-    override val menuTitleResId: Int = -1
-
-    override val menuIcon: ImageVector get() = Icons.Filled.Info
-
     @TvPreview
     @Composable
     override fun Compose() = super.Compose()
 
     @Composable
     override fun Compose(
-        navController: NavHostControllerEx?,
+        navController: NavHostControllerEx,
         backStackEntry: NavBackStackEntry?,
-        menuItems: List<MenuItem>,
         args: Map<String, Any?>
     ) {
 

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

@@ -8,28 +8,41 @@
 
 package org.mjdev.tvapp.ui.screens
 
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+import org.mjdev.tvapp.R
 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.ui.components.navigation.ScreenWithPages
+import org.mjdev.tvapp.base.ui.components.page.PagerScope
+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.SubscriptionPage
 
 class MainScreen : ScreenWithPages() {
 
-    override val startPage: Int = 1
+    override val title: Int = R.string.app_name
+    override val menuIcon: ImageVector = Icons.Default.Home
+    override val startPageIndex: Int = 2
+    override val immersive: Boolean = false
 
     @TvPreview
     @Composable
     override fun Compose() = super.Compose()
 
-    override val pages: @Composable (PagerScope.() -> Unit) = {
+    override val pages: (PagerScope.() -> Unit) = {
+
+        page(SearchPage())
 
         page(AboutPage())
         page(MainPage())
         page(SubscriptionPage())
 
+        page(SettingsPage())
+
     }
 
 }

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

@@ -21,11 +21,11 @@ import androidx.navigation.NavType
 import androidx.navigation.navArgument
 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.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.base.ui.components.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.ui.components.navigation.Screen
 import org.mjdev.tvapp.state.DetailsLoadingState
 import org.mjdev.tvapp.ui.components.Loading
 import org.mjdev.tvapp.ui.components.MediaPlayer
@@ -35,7 +35,9 @@ class PlayerScreen : Screen() {
 
     private val movieId = "movieId"
 
-    override val titleResId = R.string.title_movie_player
+    override val title = R.string.title_movie_player
+    override val menuIcon: ImageVector get() = Icons.Filled.PlayArrow
+    override val immersive: Boolean = true
 
     override val args = listOf(
         navArgument(movieId) {
@@ -44,23 +46,20 @@ class PlayerScreen : Screen() {
         }
     )
 
-    override val menuTitleResId: Int = -1
-
-    override val menuIcon: ImageVector get() = Icons.Filled.PlayArrow
-
     @TvPreview
     @Composable
     override fun Compose() = super.Compose()
 
     @Composable
     override fun Compose(
-        navController: NavHostControllerEx?,
+        navController: NavHostControllerEx,
         backStackEntry: NavBackStackEntry?,
-        menuItems: List<MenuItem>,
         args: Map<String, Any?>
     ) {
 
-        val viewModel : DetailViewModel = appViewModel { context ->
+        val isEdit = isEditMode()
+
+        val viewModel: DetailViewModel = appViewModel { context ->
             DetailViewModel.mockDetailViewModel(context)
         }
 
@@ -69,21 +68,17 @@ class PlayerScreen : Screen() {
 
         fun postError(error: String) = viewModel.postError(error.asException())
 
-        when (movieState.value) {
-            is DetailsLoadingState.Ready -> MediaPlayer(
+        if (isEdit || (movieState.value is DetailsLoadingState.Ready)) {
+            MediaPlayer(
                 modifier = Modifier.fillMaxSize(),
                 movie = (movieState.value as DetailsLoadingState.Ready).movie
             )
-
-            is DetailsLoadingState.NotFound -> {
-                postError("Movie for id: $movieId not found.")
-                Loading()
-            }
-
-            else -> {
-                postError("Movie for id: $movieId not found.")
-                Loading()
-            }
+        } else if (movieState.value is DetailsLoadingState.NotFound) {
+            postError("Movie for id: $movieId not found.")
+            Loading()
+        } else {
+            postError("Movie for id: $movieId not found.")
+            Loading()
         }
 
     }

+ 54 - 59
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt

@@ -11,7 +11,6 @@ package org.mjdev.tvapp.ui.screens
 import android.view.animation.OvershootInterpolator
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
@@ -26,7 +25,6 @@ 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
@@ -37,15 +35,17 @@ 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.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.ui.components.complex.ScreenView
+import org.mjdev.tvapp.base.ui.components.navigation.Screen
 import org.mjdev.tvapp.base.ui.components.icon.IconAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 class SplashScreen : Screen() {
 
     override val showOnce: Boolean = true
+    override val backgroundColor: Color = Color.Black
+    override val immersive: Boolean = true
 
     @TvPreview
     @Composable
@@ -53,9 +53,8 @@ class SplashScreen : Screen() {
 
     @Composable
     override fun Compose(
-        navController: NavHostControllerEx?,
+        navController: NavHostControllerEx,
         backStackEntry: NavBackStackEntry?,
-        menuItems: List<MenuItem>,
         args: Map<String, Any?>
     ) {
 
@@ -72,61 +71,57 @@ class SplashScreen : Screen() {
                     })
             )
             delay(3000L)
-            navController?.addOnDestinationChangedListener(this@SplashScreen)
-            navController?.openClear<MainScreen>()
+            navController.addOnDestinationChangedListener(this@SplashScreen)
+            navController.open<MainScreen>()
         }
 
-        Box(
-            modifier = Modifier
-                .scale(scale.value)
-                .fillMaxSize()
-                .background(Color.Black, RectangleShape),
-            contentAlignment = Alignment.Center
+        ScreenView(
+            backgroundColor = backgroundColor
         ) {
-
-
-            IconAny(
-                modifier = Modifier.size(128.dp),
-                src = Icons.Sharp.Tv,
-                tint = Color.White.copy(alpha = 0.5f)
-            )
-
-            TextAny(
-                text = R.string.app_name,
-                fontSize = 64.sp,
-                fontWeight = FontWeight.Bold,
-                color = Color.White,
-                modifier = Modifier.padding(10.dp),
-                style = MaterialTheme.typography.bodyMedium.copy(
-                    shadow = Shadow(
-                        color = Color.Gray,
-                        offset = Offset(5f, 5f),
-                        blurRadius = 10f
-                    )
-                ),
-            )
-
-        }
-
-        Box(
-            modifier = Modifier
-                .fillMaxSize()
-                .padding(
-                    0.dp,
-                    0.dp,
-                    0.dp,
-                    4.dp
-                ),
-            contentAlignment = Alignment.BottomCenter
-        ) {
-
-            TextAny(
-                text = R.string.author,
-                fontSize = 14.sp,
-                fontWeight = FontWeight.Bold,
-                color = Color.White.copy(alpha = 0.5f)
-            )
-
+            Box(
+                modifier = Modifier
+                    .scale(scale.value)
+                    .fillMaxSize(),
+                contentAlignment = Alignment.Center
+            ) {
+                IconAny(
+                    modifier = Modifier.size(128.dp),
+                    src = Icons.Sharp.Tv,
+                    tint = Color.White.copy(alpha = 0.5f)
+                )
+                TextAny(
+                    text = R.string.app_name,
+                    fontSize = 64.sp,
+                    fontWeight = FontWeight.Bold,
+                    color = Color.White,
+                    modifier = Modifier.padding(10.dp),
+                    style = MaterialTheme.typography.bodyMedium.copy(
+                        shadow = Shadow(
+                            color = Color.Gray,
+                            offset = Offset(5f, 5f),
+                            blurRadius = 10f
+                        )
+                    ),
+                )
+            }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(
+                        0.dp,
+                        0.dp,
+                        0.dp,
+                        4.dp
+                    ),
+                contentAlignment = Alignment.BottomCenter
+            ) {
+                TextAny(
+                    text = R.string.author,
+                    fontSize = 14.sp,
+                    fontWeight = FontWeight.Bold,
+                    color = Color.White.copy(alpha = 0.5f)
+                )
+            }
         }
 
     }

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

@@ -12,21 +12,26 @@
 
     <string name="app_name">Tv App</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="bt_dismiss">Dismiss</string>
 
     <string name="error_no_network">Internet connection is missing.</string>
     <string name="error_unknown">Unknown error.</string>
 
-    <string name="bt_dismiss">Dismiss</string>
+    <string name="menu_title_search">Search</string>
+    <string name="menu_title_settings">Settings</string>
+    <string name="menu_title_exit">Exit</string>
+    <string name="menu_title_home">Home</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_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="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="author">Created by Milan Jurkulak © 2023</string>
 
 </resources>