Milan Jurkulak 3 роки тому
батько
коміт
241d1e7cd7

+ 4 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItem.kt

@@ -21,8 +21,9 @@ package org.mjdev.tvapp.base.navigation
  */
 data class MenuItem(
 
-    val menuText: Any?,
-    val menuIcon: Any?,
-    var route: String
+    val menuText: Any? = null,
+    val menuIcon: Any? = null,
+    var route: String? = null,
+    var pagerIndex: Int? = null
 
 )

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

@@ -38,7 +38,6 @@ import androidx.navigation.NavDestination
 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
 
@@ -75,7 +74,7 @@ open class Screen : NavController.OnDestinationChangedListener {
     /**
      * Screen title
      */
-    open val titleResId: Int = R.string.app_name
+    open val titleResId: Int = -1
 
     /**
      * Screen arguments if any

+ 35 - 18
app/src/main/kotlin/org/mjdev/tvapp/base/page/Page.kt

@@ -1,5 +1,6 @@
 package org.mjdev.tvapp.base.page
 
+import android.annotation.SuppressLint
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
@@ -7,31 +8,47 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
-import org.mjdev.tvapp.R
-import org.mjdev.tvapp.base.annotations.TvPreview
+import androidx.navigation.NavHostController
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
+import org.mjdev.tvapp.base.navigation.MenuItem
 import org.mjdev.tvapp.base.state.ScreenState
 
-@TvPreview
-@Composable
-fun Page(
-    screenState: ScreenState? = null,
-    title: Any? = R.string.app_name,
-    block: @Composable () -> Unit = {},
-) {
+abstract class Page {
 
-    Column(
-        Modifier
-            .fillMaxSize()
-            .background(Color.DarkGray, RectangleShape).touchable {
-               freeFocus()
-            }
-    ) {
 
-        screenState?.titleState?.value = title
+    open val title: Any? = null
+    open val icon: Any? = null
 
-        block()
+    var navController: NavHostController? = null
+    var screenState: ScreenState? = null
+
+    lateinit var pagerState: PagerState
+
+    val menuItem get() = title?.let { t -> MenuItem(t, icon) }
+
+    @SuppressLint("ComposableNaming")
+    @Composable
+    fun content() {
+
+        Column(
+            Modifier
+                .fillMaxSize()
+                .background(Color.DarkGray, RectangleShape)
+                .touchable {
+                    freeFocus()
+                }
+        ) {
+
+            screenState?.titleState?.value = title
+
+            Content()
+
+        }
 
     }
 
+    @Composable
+    abstract fun Content()
+
+
 }

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

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

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

@@ -1,28 +1,51 @@
 package org.mjdev.tvapp.base.page
 
+import android.annotation.SuppressLint
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
-import org.mjdev.tvapp.base.annotations.TvPreview
+import androidx.navigation.NavHostController
+import org.mjdev.tvapp.base.navigation.MenuItem
 import org.mjdev.tvapp.base.state.ScreenState
 
-@TvPreview
+@SuppressLint("AutoboxingStateValueProperty")
 @Composable
 fun Pager(
+    navController: NavHostController? = null,
     screenState: ScreenState? = null,
-    pages: @Composable () -> Unit = {}
+    menuItems: MutableList<MenuItem> = mutableListOf(),
+    startIndex: Int = 0,
+    pages: @Composable PagerScope.(pagerState: PagerState) -> Unit = {}
 ) {
+
+    val pagerScope = PagerScope(navController, screenState, menuItems)
+    val pageState = remember { PagerState(startIndex) }
+
     Column(
         modifier = Modifier
             .fillMaxSize()
             .background(Color.DarkGray, RectangleShape)
     ) {
 
-        pages()
+        pages.invoke(pagerScope, pageState)
+
+        val index = pageState.index.value
+
+        pagerScope[index].let { page ->
+
+            page.navController = navController
+            page.screenState = screenState
+            page.pagerState = pageState
+
+            page.content()
+
+        }
 
     }
+
 }

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

@@ -0,0 +1,32 @@
+package org.mjdev.tvapp.base.page
+
+import androidx.navigation.NavHostController
+import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.state.ScreenState
+
+class PagerScope(
+    val navController: NavHostController? = null,
+    val screenState: ScreenState? = null,
+    val menuItems: MutableList<MenuItem> = mutableListOf(),
+) : ArrayList<Page>() {
+
+    fun page(page: Page) {
+        page.navController = navController
+        page.screenState = screenState
+        page.menuItem?.also { menuItem ->
+            menuItems.firstOrNull { mi -> mi.menuText == page.title }.also { duplicate ->
+                if (duplicate != null) menuItems.remove(duplicate)
+            }
+            menuItems.add(menuItem)
+        }
+        addUnique(page)
+    }
+
+    private fun addUnique(page: Page) {
+        firstOrNull { p -> p.title == page.title }.also { duplicate ->
+            if (duplicate != null) remove(duplicate)
+        }
+        add(page)
+    }
+
+}

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

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

+ 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."))
 
     }
 

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

@@ -1,29 +1,35 @@
 package org.mjdev.tvapp.ui.pages
 
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Info
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.navigation.NavHostController
+import androidx.compose.ui.graphics.vector.ImageVector
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.page.Page
-import org.mjdev.tvapp.base.state.ScreenState
 import org.mjdev.tvapp.base.ui.components.image.CircleImage
 
-@TvPreview
-@Composable
-fun AboutPage(
-    navController: NavHostController? = null,
-    screenState: ScreenState? = null,
-) = Page(
-    title = R.string.title_about
-) {
+class AboutPage : Page() {
 
-    Column {
+    override val title: Int = R.string.title_about
+    override val icon: ImageVector = Icons.Default.Info
 
-        CircleImage(
-            src = Color.Black
-        )
+    @TvPreview
+    @Composable
+    override fun Content() {
+
+        Column {
+
+            CircleImage(
+                modifier = Modifier.fillMaxSize(),
+                src = Color.Black
+            )
+
+        }
 
     }
 

+ 92 - 84
app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

@@ -1,5 +1,6 @@
 package org.mjdev.tvapp.ui.pages
 
+import android.annotation.SuppressLint
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
@@ -7,15 +8,17 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
-import androidx.navigation.NavHostController
 import androidx.tv.foundation.lazy.list.TvLazyRow
 import androidx.tv.foundation.lazy.list.items
 import androidx.tv.material3.Text
@@ -27,7 +30,6 @@ import org.mjdev.tvapp.base.extensions.StringExt.asException
 import org.mjdev.tvapp.base.navigation.Screen.Companion.open
 import org.mjdev.tvapp.base.network.NetworkStatus
 import org.mjdev.tvapp.base.page.Page
-import org.mjdev.tvapp.base.state.ScreenState
 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
@@ -39,114 +41,120 @@ import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.PlayerScreen
 import org.mjdev.tvapp.viewmodel.MainViewModel
 
-@TvPreview
-@Composable
-fun MainPage(
-    navController: NavHostController? = null,
-    screenState: ScreenState? = null,
-) = Page {
-
-    val viewModel = appViewModel<MainViewModel>()
-
-    val categoryList = remember { viewModel.categoryList }.collectAsState()
-    val featuredMovieList = remember { viewModel.featuredMovieList }.collectAsState()
-    val messages = remember { viewModel.messages }.collectAsState()
-    val networkState = remember {
-        viewModel.networkInfo.networkStatus
-    }.collectAsState(null)
-
-    val onItemClick: (movie: Movie?) -> Unit = { movie ->
-        if (movie == null) {
-            viewModel.postError("No media found.".asException())
-        } else if (movie.hasVideoUri) {
-            navController?.open<PlayerScreen>(movie.id)
-        } else {
-            navController?.open<DetailScreen>(movie.id)
-        }
-    }
+@SuppressLint("ComposableNaming")
+class MainPage: Page() {
 
-    viewModel.handleError { error ->
-        screenState?.error(error)
-    }
+    override val title: Int = R.string.app_name
+    override val icon: ImageVector = Icons.Default.Home
 
-    ScrollableTvLazyRow(
-        modifier = Modifier
-            .fillMaxSize()
-            .background(Color.DarkGray),
-        verticalArrangement = Arrangement.spacedBy(32.dp),
-        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
-    ) {
+    @TvPreview
+    @Composable
+    override fun Content() {
 
-        item {
+        val viewModel = appViewModel<MainViewModel>()
 
-            Header(
-                title = screenState?.titleState?.value,
-                messagesCount = messages.value.size
-            )
+        val categoryList = remember { viewModel.categoryList }.collectAsState()
+        val featuredMovieList = remember { viewModel.featuredMovieList }.collectAsState()
+        val messages = remember { viewModel.messages }.collectAsState()
+        val networkState = remember {
+            viewModel.networkInfo.networkStatus
+        }.collectAsState(null)
 
+        val onItemClick: (movie: Movie?) -> Unit = { movie ->
+            if (movie == null) {
+                viewModel.postError("No media found.".asException())
+            } else if (movie.hasVideoUri) {
+                navController?.open<PlayerScreen>(movie.id)
+            } else {
+                navController?.open<DetailScreen>(movie.id)
+            }
         }
 
-        if (networkState.value !is NetworkStatus.Connected) item {
-            ErrorMessage(
-                error = stringResource(R.string.error_no_network).asException(),
-                backgroundColor = Color.Black,
-                dismissible = false
-            )
+        viewModel.handleError { error ->
+            screenState?.error(error)
         }
 
+        ScrollableTvLazyRow(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(Color.DarkGray),
+            verticalArrangement = Arrangement.spacedBy(32.dp),
+            contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
+        ) {
 
-        if (categoryList.value.isNotEmpty()) item {
+            item {
 
-            Tabs(
-                items = categoryList.value.map { it.name }
-            )
+                Header(
+                    title = screenState?.titleState?.value,
+                    messagesCount = messages.value.size
+                )
 
-        }
+            }
 
-        if (featuredMovieList.value.isNotEmpty()) item {
+            if (networkState.value !is NetworkStatus.Connected) item {
+                ErrorMessage(
+                    error = stringResource(R.string.error_no_network).asException(),
+                    backgroundColor = Color.Black,
+                    dismissible = false
+                )
+            }
 
-            BigCarousel(
-                modifier = Modifier.touchable(),
-                items = featuredMovieList.value,
-                onItemClicked = onItemClick
-            )
 
-        }
+            if (categoryList.value.isNotEmpty()) item {
 
-        items(categoryList.value) { category ->
-
-            Text(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(0.dp, 16.dp),
-                text = category.name,
-                color = Color.White,
-                fontWeight = FontWeight.Bold
-            )
-
-            TvLazyRow(
-                horizontalArrangement = Arrangement.spacedBy(16.dp),
-                modifier = Modifier.height(200.dp),
-                contentPadding = PaddingValues(
-                    start = 16.dp,
-                    top = 16.dp,
-                    bottom = 16.dp,
-                    end = 16.dp
+                Tabs(
+                    items = categoryList.value.map { it.name }
                 )
-            ) {
 
-                items(category.movieList) { item ->
+            }
+
+            if (featuredMovieList.value.isNotEmpty()) item {
 
-                    MovieCard(
-                        movie = item,
-                        onClick = onItemClick
+                BigCarousel(
+                    modifier = Modifier.touchable(),
+                    items = featuredMovieList.value,
+                    onItemClicked = onItemClick
+                )
+
+            }
+
+            items(categoryList.value) { category ->
+
+                Text(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .padding(0.dp, 16.dp),
+                    text = category.name,
+                    color = Color.White,
+                    fontWeight = FontWeight.Bold
+                )
+
+                TvLazyRow(
+                    horizontalArrangement = Arrangement.spacedBy(16.dp),
+                    modifier = Modifier.height(200.dp),
+                    contentPadding = PaddingValues(
+                        start = 16.dp,
+                        top = 16.dp,
+                        bottom = 16.dp,
+                        end = 16.dp
                     )
+                ) {
+
+                    items(category.movieList) { item ->
+
+                        MovieCard(
+                            movie = item,
+                            onClick = onItemClick
+                        )
+
+                    }
 
                 }
 
             }
 
         }
+
     }
 
 }

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

@@ -2,31 +2,34 @@ package org.mjdev.tvapp.ui.pages
 
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ShoppingCart
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.navigation.NavHostController
+import androidx.compose.ui.graphics.vector.ImageVector
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.page.Page
-import org.mjdev.tvapp.base.state.ScreenState
 import org.mjdev.tvapp.base.ui.components.image.CircleImage
 
-@TvPreview
-@Composable
-fun SubscriptionPage(
-    navController: NavHostController? = null,
-    screenState: ScreenState? = null,
-) = Page(
-    title = R.string.title_subscription,
-) {
-
-    Column(
-        Modifier.fillMaxSize()
-    ) {
-
-        CircleImage(
-            src = R.drawable.milanj
-        )
+class SubscriptionPage : Page() {
+
+    override val title : Int = R.string.title_subscription
+    override val icon : ImageVector = Icons.Default.ShoppingCart
+
+    @TvPreview
+    @Composable
+    override fun Content () {
+
+        Column(
+            Modifier.fillMaxSize()
+        ) {
+
+            CircleImage(
+                src = R.drawable.milanj
+            )
+
+        }
 
     }
 

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

@@ -8,10 +8,8 @@
 
 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 androidx.compose.runtime.remember
 import androidx.compose.ui.res.stringResource
 import androidx.navigation.NamedNavArgument
 import androidx.navigation.NavBackStackEntry
@@ -22,18 +20,14 @@ import org.mjdev.tvapp.base.navigation.MenuItem
 import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.base.page.Pager
 import org.mjdev.tvapp.base.ui.components.complex.ScreenView
+import org.mjdev.tvapp.ui.pages.AboutPage
 import org.mjdev.tvapp.ui.pages.MainPage
+import org.mjdev.tvapp.ui.pages.SubscriptionPage
 
 class MainScreen : Screen() {
 
-    override val titleResId = R.string.app_name
-
     override val args = listOf<NamedNavArgument>()
 
-    override val menuTitleResId: Int = R.string.menu_item_home
-
-    override val menuIcon: ImageVector get() = Icons.Filled.Home
-
     @TvPreview
     @Composable
     override fun Compose() = super.Compose()
@@ -46,17 +40,23 @@ class MainScreen : Screen() {
         args: Map<String, Any?>
     ) {
 
+        val menuState = remember { mutableListOf<MenuItem>().apply { addAll(menuItems) } }
+
         ScreenView(
             navController = navController,
-            title = stringResource(titleResId),
-            menuItems = menuItems
+            title = if (titleResId > -1) stringResource(titleResId) else R.string.app_name,
+            menuItems = menuState
         ) { screenState ->
 
             Pager(
-                screenState = screenState
+                navController,
+                screenState,
+                menuState
             ) {
 
-                MainPage(navController, screenState)
+                page(MainPage())
+                page(AboutPage())
+                page(SubscriptionPage())
 
             }
 

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

@@ -14,6 +14,7 @@ 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
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.sharp.Tv
@@ -98,8 +99,14 @@ class SplashScreen : Screen() {
 
         Box(
             modifier = Modifier
-                .fillMaxSize(),
-            contentAlignment = Alignment.BottomEnd
+                .fillMaxSize()
+                .padding(
+                    0.dp,
+                    0.dp,
+                    0.dp,
+                    4.dp
+                ),
+            contentAlignment = Alignment.BottomCenter
         ) {
 
             TextAny(

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

@@ -12,17 +12,15 @@
 
     <string name="app_name">Tv App</string>
 
-    <string name="title_movie_detail">movie detail</string>
-    <string name="title_about">about</string>
-    <string name="title_movie_player">movie player</string>
-    <string name="title_subscription">subscription</string>
-    <string name="title_error">error</string>
+    <string name="title_movie_detail">Movie detail</string>
+    <string name="title_about">About</string>
+    <string name="title_movie_player">Movie player</string>
+    <string name="title_subscription">Subscription</string>
+    <string name="title_error">Error</string>
 
     <string name="error_unknown">Unknown error.</string>
 
-    <string name="dismiss">dismiss</string>
-
-    <string name="menu_item_home">home</string>
+    <string name="dismiss">Dismiss</string>
 
     <string name="error_no_network">Internet connection is missing.</string>