|
|
@@ -0,0 +1,241 @@
|
|
|
+/*
|
|
|
+ * 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.annotation.CallSuper
|
|
|
+import androidx.compose.foundation.layout.Arrangement
|
|
|
+import androidx.compose.foundation.layout.Column
|
|
|
+import androidx.compose.foundation.layout.fillMaxSize
|
|
|
+import androidx.compose.runtime.Composable
|
|
|
+import androidx.compose.ui.Alignment
|
|
|
+import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.ui.graphics.Color
|
|
|
+import androidx.compose.ui.graphics.vector.ImageVector
|
|
|
+import androidx.compose.ui.unit.sp
|
|
|
+import androidx.navigation.NamedNavArgument
|
|
|
+import androidx.navigation.NavBackStackEntry
|
|
|
+import androidx.navigation.NavHostController
|
|
|
+import androidx.navigation.compose.composable
|
|
|
+import androidx.tv.material3.Text
|
|
|
+import org.mjdev.tvapp.R
|
|
|
+import org.mjdev.tvapp.base.annotations.TvPreview
|
|
|
+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")
|
|
|
+open class Screen {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Autogenerated path / route
|
|
|
+ */
|
|
|
+ private val routeBase: String
|
|
|
+ get() = (this::class.simpleName ?: "-") + "?"
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Complete autogenerated path / route with parameters
|
|
|
+ */
|
|
|
+ val completeRoute: String
|
|
|
+ get() = routeBase.let { rb ->
|
|
|
+ var routeImpl = rb
|
|
|
+ args.forEach { arg ->
|
|
|
+ routeImpl = routeImpl.plus("${arg.name}={${arg.name}}")
|
|
|
+ }
|
|
|
+ routeImpl
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Screen title
|
|
|
+ */
|
|
|
+ open val titleResId: Int = R.string.app_name
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Screen arguments if any
|
|
|
+ */
|
|
|
+ open val args: List<NamedNavArgument> = emptyList()
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Menu string resource id
|
|
|
+ * menu is generated only if it returns id > 0
|
|
|
+ */
|
|
|
+ open val menuTitleResId: Int = -1
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Menu icon if any
|
|
|
+ */
|
|
|
+ open val menuIcon: ImageVector? = null
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Autogenerated menu item, if any
|
|
|
+ * Generated only if [menuTitleResId] > 0
|
|
|
+ */
|
|
|
+ val menuItem
|
|
|
+ get() = if (menuTitleResId >= 0) MenuItem(menuTitleResId, menuIcon, completeRoute)
|
|
|
+ else null
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Compose function.
|
|
|
+ *
|
|
|
+ * Helper function to preview without parameters as preview of compose needed.
|
|
|
+ *
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @TvPreview
|
|
|
+ @Composable
|
|
|
+ @CallSuper
|
|
|
+ open fun Compose() {
|
|
|
+ Compose(null, null, emptyList(), mapOf())
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Compose function for compose screen.
|
|
|
+ * This should be only a method overriden in super classes.
|
|
|
+ *
|
|
|
+ * @param navController Nav controller
|
|
|
+ * @param backStackEntry Back stack entry
|
|
|
+ * @param menuItems Menu items
|
|
|
+ */
|
|
|
+ @Composable
|
|
|
+ open fun Compose(
|
|
|
+ navController: NavHostController?,
|
|
|
+ backStackEntry: NavBackStackEntry?,
|
|
|
+ menuItems: List<MenuItem>,
|
|
|
+ args: Map<String, Any?>
|
|
|
+ ) {
|
|
|
+
|
|
|
+ Column(
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ verticalArrangement = Arrangement.Center,
|
|
|
+ horizontalAlignment = Alignment.CenterHorizontally
|
|
|
+ ) {
|
|
|
+ Text(
|
|
|
+ text = "Empty Screen",
|
|
|
+ fontSize = 32.sp,
|
|
|
+ color = Color.White
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Helper function to get params from route.
|
|
|
+ *
|
|
|
+ * @param argId Arg id
|
|
|
+ * @param defaultValue Default value
|
|
|
+ * @return
|
|
|
+ * @receiver [NavBackStackEntry]
|
|
|
+ */
|
|
|
+ @Suppress("UNCHECKED_CAST", "DEPRECATION")
|
|
|
+ private fun <T> NavBackStackEntry.arg(
|
|
|
+ argId: String,
|
|
|
+ defaultValue: T
|
|
|
+ ): T = (arguments?.get(argId) as? T?) ?: defaultValue
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Open screen extension.
|
|
|
+ *
|
|
|
+ * Function opens a screen with parameters given, if any,
|
|
|
+ * from another screen.
|
|
|
+ *
|
|
|
+ * @param navController Nav controller
|
|
|
+ * @param values Values
|
|
|
+ * @param T T
|
|
|
+ */
|
|
|
+ inline fun <reified T : Screen> open(
|
|
|
+ navController: NavHostController?,
|
|
|
+ vararg values: Any?
|
|
|
+ ) {
|
|
|
+ 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 ->
|
|
|
+ val currentRoute = navController?.currentRoute
|
|
|
+ val equals = currentRoute?.equals(finalRoute)
|
|
|
+ if (equals != true) {
|
|
|
+ navController?.navigate(finalRoute)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Current route nav host helper
|
|
|
+ *
|
|
|
+ * Function returns current route that is shown.
|
|
|
+ *
|
|
|
+ * @return current route on screen
|
|
|
+ */
|
|
|
+ val NavHostController.currentRoute
|
|
|
+ get() = currentDestination?.route
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Open route.
|
|
|
+ *
|
|
|
+ * @param route Route
|
|
|
+ * @receiver [NavHostController]
|
|
|
+ */
|
|
|
+ fun NavHostController.open(route: String) {
|
|
|
+ val equals = currentRoute?.equals(route)
|
|
|
+ if (equals != true) {
|
|
|
+ navigate(route)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Custom function to generate screen in nav graph.
|
|
|
+ *
|
|
|
+ * @param route Route route expected
|
|
|
+ * @param isHomeScreen Is home screen, indicates that this screen will
|
|
|
+ * be used as home screen. Should be used once, or in case that is defined multiple times,
|
|
|
+ * the last one is used as home screen.
|
|
|
+ *
|
|
|
+ * @param T T Screen object generated for route.
|
|
|
+ * @receiver [NavGraphBuilderEx]
|
|
|
+ */
|
|
|
+ fun <T : Screen> NavGraphBuilderEx.screen(
|
|
|
+ route: T,
|
|
|
+ isHomeScreen: Boolean = false
|
|
|
+ ) {
|
|
|
+ if (isHomeScreen) {
|
|
|
+ startDestinationRouteEx = route.completeRoute
|
|
|
+ }
|
|
|
+ route.menuItem?.also { menuItem ->
|
|
|
+ menuItems.add(menuItem)
|
|
|
+ }
|
|
|
+ composable(
|
|
|
+ route = route.completeRoute,
|
|
|
+ arguments = route.args
|
|
|
+ ) { be ->
|
|
|
+ val rArgs = mutableMapOf<String, Any?>().apply {
|
|
|
+ route.args.forEach { arg ->
|
|
|
+ put(arg.name, be.arg(arg.name, null))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ route.Compose(navHostController, be, menuItems, rArgs)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|