Milan Jurkulak před 3 roky
rodič
revize
0d8cac1ebf
51 změnil soubory, kde provedl 1012 přidání a 313 odebrání
  1. 2 0
      app/build.gradle.kts
  2. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt
  3. 4 4
      app/src/main/kotlin/org/mjdev/tvapp/base/activity/ComposableActivity.kt
  4. 3 2
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ColorFilterExt.kt
  5. 9 5
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ModifierExt.kt
  6. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/NavExt.kt
  7. 186 0
      app/src/main/kotlin/org/mjdev/tvapp/base/helpers/BrushBuilder.kt
  8. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItem.kt
  9. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/MenuItemClickListener.kt
  10. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavGraphBuilderEx.kt
  11. 17 1
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostControllerEx.kt
  12. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavigationState.kt
  13. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/Screen.kt
  14. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/navigation/ScreenWithPages.kt
  15. 11 4
      app/src/main/kotlin/org/mjdev/tvapp/base/network/NetworkStatus.kt
  16. 6 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/badge/Badge.kt
  17. 3 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/button/Button.kt
  18. 17 19
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/ItemCard.kt
  19. 5 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/PhotoCard.kt
  20. 233 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/PreviewCard.kt
  21. 8 13
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/carousel/BigCarousel.kt
  22. 0 4
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableBox.kt
  23. 0 10
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableCard.kt
  24. 10 10
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavDrawerContent.kt
  25. 6 4
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavHostEx.kt
  26. 38 24
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt
  27. 37 13
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavigationRow.kt
  28. 37 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/SettingsDrawer.kt
  29. 2 3
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Page.kt
  30. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/PagerScope.kt
  31. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/SearchPage.kt
  32. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/SettingsPage.kt
  33. 3 3
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/TvPager.kt
  34. 2 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/screen/EmptyScreen.kt
  35. 7 6
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/screen/ScreenView.kt
  36. 57 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/text/TextWithShadow.kt
  37. 105 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/BrowseView.kt
  38. 58 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/CategoryRow.kt
  39. 6 6
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Clock.kt
  40. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/ErrorMessage.kt
  41. 6 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Header.kt
  42. 6 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/ScrollableTvLazyRow.kt
  43. 62 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/TVRow.kt
  44. 6 5
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Tabs.kt
  45. 12 27
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Title.kt
  46. 6 6
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/UserPic.kt
  47. 14 91
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  48. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  49. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/MainScreen.kt
  50. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/PlayerScreen.kt
  51. 9 16
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt

+ 2 - 0
app/build.gradle.kts

@@ -178,6 +178,8 @@ dependencies {
     implementation("androidx.media3:media3-decoder:1.0.2")
     implementation("androidx.media3:media3-datasource:1.0.2")
     implementation("androidx.media3:media3-common:1.0.2")
+//    implementation ("com.rubensousa.dpadrecyclerview:dpadrecyclerview:1.1.0-alpha02")
+//    implementation ("com.rubensousa.dpadrecyclerview:dpadrecyclerview-compose:1.1.0-alpha02")
 
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0")?.apply {

+ 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.ui.components.navigation.NavGraphBuilderEx
-import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.screen
+import org.mjdev.tvapp.base.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.base.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

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

@@ -30,10 +30,10 @@ import androidx.tv.material3.Surface
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ContextExt.isATv
 import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
-import org.mjdev.tvapp.base.ui.components.complex.EmptyScreen
-import org.mjdev.tvapp.base.ui.components.navigation.NavGraphBuilderEx
-import org.mjdev.tvapp.base.ui.components.complex.NavHostEx
-import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.screen
+import org.mjdev.tvapp.base.ui.components.screen.EmptyScreen
+import org.mjdev.tvapp.base.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.base.ui.components.navigation.NavHostEx
+import org.mjdev.tvapp.base.navigation.Screen.Companion.screen
 import org.mjdev.tvapp.ui.theme.TVAppTheme
 import timber.log.Timber
 

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

@@ -6,6 +6,8 @@
  *  w: https://mjdev.org
  */
 
+@file:Suppress("unused")
+
 package org.mjdev.tvapp.base.extensions
 
 import androidx.annotation.FloatRange
@@ -30,7 +32,7 @@ object ColorFilterExt {
         )
     )
 
-    fun BW() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) })
+    fun bw() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) })
 
     fun colorInvert() = ColorFilter.colorMatrix(
         ColorMatrix(
@@ -43,5 +45,4 @@ object ColorFilterExt {
         )
     )
 
-
 }

+ 9 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ModifierExt.kt

@@ -16,7 +16,6 @@ import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.focusRequester
@@ -36,21 +35,25 @@ object ModifierExt {
         mutableStateOf(initial)
     }
 
+    @Composable
+    fun rememberFocusRequester() = remember { FocusRequester() }
+
     val MutableState<FocusState?>.isFocused
         get() = (value?.isFocused == true) || (value?.hasFocus == true)
 
     @SuppressLint("ComposableModifierFactory")
     @Composable
+    // todo focus problem, requester not registered
     fun Modifier.touchable(
         state: MutableState<FocusState?> = rememberFocusState(),
-        focusRequester: FocusRequester = remember { FocusRequester() },
-        onFocus:()->Unit={},
-        onClick:()->Unit={},
+        focusRequester: FocusRequester = rememberFocusRequester(),
+        onFocus: () -> Unit = {},
+        onClick: () -> Unit = {},
     ): Modifier = focusRequester(
         focusRequester
     ).pointerInput(Unit) {
         detectTapGestures {
-            if(state.isFocused) {
+            if (state.isFocused) {
                 onClick()
             } else {
                 try {
@@ -63,6 +66,7 @@ object ModifierExt {
         }
     }.onFocusChanged { fstate ->
         state.value = fstate
+        onFocus()
     }.apply {
         if (state.isFocused) {
             try {

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

+ 186 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/BrushBuilder.kt

@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("unused")
+
+package org.mjdev.tvapp.base.helpers
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TileMode
+import kotlin.math.abs
+import kotlin.math.cos
+import kotlin.math.sin
+
+class BrushBuilder {
+
+    private var type: Int = 1
+    private var colorStops: Array<out Pair<Float, Color>> = emptyArray()
+    private var start: Float = 0f
+    private var end: Float = Float.POSITIVE_INFINITY
+    private var tileMode: TileMode = TileMode.Clamp
+
+    fun build(): Brush? = when (type) {
+        Vertical -> {
+            Brush.verticalGradient(
+                colorStops = colorStops,
+                startY = start,
+                endY = end,
+                tileMode = tileMode
+            )
+        }
+
+        Horizontal -> {
+            Brush.horizontalGradient(
+                colorStops = colorStops,
+                startX = start,
+                endX = end,
+                tileMode = tileMode
+            )
+        }
+
+        else -> null
+    }
+
+    fun type(type: Int): BrushBuilder = apply {
+        this.type = type
+    }
+
+    fun colors(vararg colors: Pair<Float, Color>) = apply {
+        this.colorStops = colors
+    }
+
+    fun start(start: Float): BrushBuilder = apply {
+        this.start = start
+    }
+
+    fun end(end: Float): BrushBuilder = apply {
+        this.end = end
+    }
+
+    companion object {
+
+        const val Vertical = 1
+        const val Horizontal = 2
+
+    }
+
+}
+
+data class GradientOffset(val start: Offset, val end: Offset)
+
+enum class GradientAngle {
+    CW0, CW45, CW90, CW135, CW180, CW225, CW270, CW315
+}
+
+fun GradientOffset(angle: GradientAngle = GradientAngle.CW0): GradientOffset {
+    return when (angle) {
+        GradientAngle.CW45 -> GradientOffset(
+            start = Offset.Zero,
+            end = Offset.Infinite
+        )
+
+        GradientAngle.CW90 -> GradientOffset(
+            start = Offset.Zero,
+            end = Offset(0f, Float.POSITIVE_INFINITY)
+        )
+
+        GradientAngle.CW135 -> GradientOffset(
+            start = Offset(Float.POSITIVE_INFINITY, 0f),
+            end = Offset(0f, Float.POSITIVE_INFINITY)
+        )
+
+        GradientAngle.CW180 -> GradientOffset(
+            start = Offset(Float.POSITIVE_INFINITY, 0f),
+            end = Offset.Zero,
+        )
+
+        GradientAngle.CW225 -> GradientOffset(
+            start = Offset.Infinite,
+            end = Offset.Zero
+        )
+
+        GradientAngle.CW270 -> GradientOffset(
+            start = Offset(0f, Float.POSITIVE_INFINITY),
+            end = Offset.Zero
+        )
+
+        GradientAngle.CW315 -> GradientOffset(
+            start = Offset(0f, Float.POSITIVE_INFINITY),
+            end = Offset(Float.POSITIVE_INFINITY, 0f)
+        )
+
+        else -> GradientOffset(
+            start = Offset.Zero,
+            end = Offset(Float.POSITIVE_INFINITY, 0f)
+        )
+    }
+}
+
+fun gradientBrushLinear(
+    vararg colors: Int
+) = Brush.linearGradient(
+    colors = colors.map { c -> Color(c) }.toList(),
+    start = Offset(Float.POSITIVE_INFINITY, 0f),
+    end = Offset(Float.POSITIVE_INFINITY, 0f)
+)
+
+fun Modifier.angledGradientBackground(
+    colorStops: Array<Pair<Float, Color>>,
+    degrees: Float
+) = this.then(
+    drawBehind {
+        val (x, y) = size
+        val degreesNormalised = (degrees % 360).let { if (it < 0) it + 360 else it }
+        val angleN = 90 - (degreesNormalised % 90)
+        val angleNRad = Math.toRadians(angleN.toDouble())
+        val hypot1 = abs((y * cos(angleNRad)))
+        val x1 = (abs((hypot1 * sin(angleNRad)))).toFloat()
+        val y1 = (abs((hypot1 * cos(angleNRad)))).toFloat()
+        val hypot2 = abs((x * cos(angleNRad)))
+        val x2 = (abs((hypot2 * cos(angleNRad)))).toFloat()
+        val y2 = (abs((hypot2 * sin(angleNRad)))).toFloat()
+        val offset = when {
+            degreesNormalised > 0f && degreesNormalised < 90f -> arrayOf(
+                0f - x1, y - y1,
+                x - x2, y + y2
+            )
+
+            degreesNormalised == 90f -> arrayOf(0f, 0f, 0f, y)
+            degreesNormalised > 90f && degreesNormalised < 180f -> arrayOf(
+                0f + x2, 0f - y2,
+                0f - x1, y - y1
+            )
+
+            degreesNormalised == 180f -> arrayOf(x, 0f, 0f, 0f)
+            degreesNormalised > 180f && degreesNormalised < 270f -> arrayOf(
+                x + x1, 0f + y1,
+                0f + x2, 0f - y2
+            )
+
+            degreesNormalised == 270f -> arrayOf(x, y, x, 0f)
+            degreesNormalised > 270f && degreesNormalised < 360f -> arrayOf(
+                x - x2, y + y2,
+                x + x1, 0f + y1
+            )
+
+            else -> arrayOf(0f, y, x, y)
+        }
+        drawRect(
+            brush = Brush.linearGradient(
+                colorStops = colorStops,
+                start = Offset(offset[0], offset[1]),
+                end = Offset(offset[2], offset[3])
+            ),
+            size = size
+        )
+    }
+)

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

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.navigation
+package org.mjdev.tvapp.base.navigation
 
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ExitToApp

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

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.navigation
+package org.mjdev.tvapp.base.navigation
 
 interface MenuItemClickListener {
 

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

@@ -6,12 +6,12 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.navigation
+package org.mjdev.tvapp.base.navigation
 
 import androidx.navigation.NavGraph
 import androidx.navigation.NavGraphBuilder
 import androidx.navigation.NavigatorProvider
-import org.mjdev.tvapp.base.ui.components.complex.EmptyScreen.Companion.ROUTE_NONE
+import org.mjdev.tvapp.base.ui.components.screen.EmptyScreen.Companion.ROUTE_NONE
 
 class NavGraphBuilderEx(
     provider: NavigatorProvider,

+ 17 - 1
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavHostControllerEx.kt → app/src/main/kotlin/org/mjdev/tvapp/base/navigation/NavHostControllerEx.kt

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.navigation
+package org.mjdev.tvapp.base.navigation
 
 import android.annotation.SuppressLint
 import android.content.Context
@@ -20,6 +20,9 @@ import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import androidx.navigation.NavHostController
+import androidx.tv.material3.DrawerState
+import androidx.tv.material3.DrawerValue
+import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvapp.base.extensions.ListExt.addUnique
 
 // todo apply lifecycle
@@ -30,6 +33,9 @@ class NavHostControllerEx(
     context
 ), LifecycleOwner, DefaultLifecycleObserver {
 
+    @OptIn(ExperimentalTvMaterial3Api::class)
+    val settingsDrawerState: DrawerState = DrawerState(DrawerValue.Closed)
+
     val menuState: MutableState<Boolean> = mutableStateOf(true)
 
     val menuItems: SnapshotStateList<MenuItem> = mutableStateListOf()
@@ -98,4 +104,14 @@ class NavHostControllerEx(
         }
     }
 
+    @OptIn(ExperimentalTvMaterial3Api::class)
+    fun openSettings() {
+        settingsDrawerState.setValue(DrawerValue.Open)
+    }
+
+    @OptIn(ExperimentalTvMaterial3Api::class)
+    fun closeSettings() {
+        settingsDrawerState.setValue(DrawerValue.Closed)
+    }
+
 }

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

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.navigation
+package org.mjdev.tvapp.base.navigation
 
 import androidx.compose.runtime.Composable
 import androidx.tv.material3.DrawerState

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

@@ -8,7 +8,7 @@
 
 @file:Suppress("MemberVisibilityCanBePrivate")
 
-package org.mjdev.tvapp.base.ui.components.navigation
+package org.mjdev.tvapp.base.navigation
 
 import androidx.annotation.CallSuper
 import androidx.annotation.MainThread

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

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

+ 11 - 4
app/src/main/kotlin/org/mjdev/tvapp/base/network/NetworkStatus.kt

@@ -9,14 +9,21 @@
 package org.mjdev.tvapp.base.network
 
 import android.net.Network
+import androidx.compose.runtime.State
 
 @Suppress("unused")
 sealed class NetworkStatus {
 
-    object Unknown: NetworkStatus()
+    object Unknown : NetworkStatus()
 
-    class Connected(val network: Network?): NetworkStatus()
+    class Connected(val network: Network?) : NetworkStatus()
 
-    object Disconnected: NetworkStatus()
+    object Disconnected : NetworkStatus()
 
-}
+}
+
+val State<NetworkStatus?>?.isConnected: Boolean
+    get() = this?.value is NetworkStatus.Connected
+
+val State<NetworkStatus?>?.isNotConnected: Boolean
+    get() = !isConnected

+ 6 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Badge.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/badge/Badge.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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.badge
 
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
@@ -24,6 +24,7 @@ import androidx.compose.ui.unit.sp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
+import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.text.CircleText
 
 @OptIn(ExperimentalTvMaterial3Api::class)

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

@@ -57,7 +57,9 @@ fun Button(
 ) {
 
     Button(
-        modifier = modifier.touchable(focusState),
+        modifier = modifier.touchable(
+            state = focusState,
+        ),
         glow = glow,
         scale = scale,
         onClick = onClick,

+ 17 - 19
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/ItemCard.kt

@@ -9,12 +9,12 @@
 package org.mjdev.tvapp.base.ui.components.card
 
 import android.annotation.SuppressLint
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.widthIn
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.tooling.preview.Preview
@@ -26,6 +26,7 @@ import androidx.tv.material3.CardGlow
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.CardShape
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvapp.base.extensions.ModifierExt
 import org.mjdev.tvapp.base.interfaces.ItemWithDescription
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.ui.components.complex.FocusableCard
@@ -45,7 +46,6 @@ fun ItemCard(
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
     placeholder: @Composable () -> Unit = {},
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     imageRenderer: @Composable (modifier: Modifier) -> Unit = {
         ImageAny(
             modifier = modifier,
@@ -55,21 +55,19 @@ fun ItemCard(
             placeholder = placeholder
         )
     },
+    focusState: MutableState<FocusState?> = ModifierExt.rememberFocusState(),
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
-) {
-
-    FocusableCard(
-        modifier = modifier
-            .widthIn(max = 320.dp)
-            .aspectRatio(16f / 9f),
-        item = item,
-        contentScale = contentScale,
-        scale = scale,
-        imageRenderer = imageRenderer,
-        placeholder = placeholder,
-        onFocus = onFocus,
-        onClick = onClick,
-    )
-
-}
+) = FocusableCard(
+    modifier = modifier
+        .widthIn(max = 320.dp)
+        .aspectRatio(16f / 9f),
+    item = item,
+    focusState = focusState,
+    contentScale = contentScale,
+    scale = scale,
+    imageRenderer = imageRenderer,
+    placeholder = placeholder,
+    onFocus = onFocus,
+    onClick = onClick,
+)

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

@@ -11,11 +11,14 @@ package org.mjdev.tvapp.base.ui.components.card
 import android.annotation.SuppressLint
 import androidx.compose.ui.Modifier
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
 import org.mjdev.tvapp.base.interfaces.ItemWithDescription
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.ui.components.image.PhotoImage
@@ -37,11 +40,13 @@ fun PhotoCard(
     },
     placeholder: @Composable () -> Unit = {},
     scale: CardScale = CardDefaults.scale(),
+    focusState: MutableState<FocusState?> = rememberFocusState(),
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
 ) = ItemCard(
     item = item,
     modifier = modifier,
+    focusState = focusState,
     contentScale = contentScale,
     scale = scale,
     imageRenderer = imageRenderer,

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

@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.ui.components.card
+
+import android.graphics.Bitmap
+import android.media.MediaMetadataRetriever
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.media3.common.C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.common.Player.Listener
+import androidx.media3.common.Player.REPEAT_MODE_ONE
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.DataSource
+import androidx.media3.datasource.DefaultHttpDataSource
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.source.ClippingMediaSource
+import androidx.media3.exoplayer.source.MediaSource
+import androidx.media3.exoplayer.source.ProgressiveMediaSource
+import androidx.media3.ui.AspectRatioFrameLayout
+import androidx.media3.ui.PlayerView
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.compose.AsyncImage
+import coil.compose.rememberAsyncImagePainter
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Suppress("unused")
+@UnstableApi
+@Composable
+fun PreviewCard(
+    modifier: Modifier = Modifier,
+    cardWidth: Dp,
+    cardHeight: Dp,
+    videoUrl: String,
+    hasFocus: Boolean,
+    clipStartPosition: Long = 2000000,
+    clipEndPosition: Long = 8000000,
+    thumbnailFrame: Long = 5000000,
+    thumbnailUrl: String? = null
+) {
+    Card(
+        modifier = modifier
+            .width(if (hasFocus) cardWidth.times(1.2f) else cardWidth)
+            .height(if (hasFocus) cardHeight.times(1.2f) else cardHeight)
+    ) {
+        val context = LocalContext.current
+        var currentlyPlaying by remember { mutableStateOf(false) }
+        var isBuffering by remember { mutableStateOf(true) }
+        var thumbnailBitmap by rememberSaveable {
+            mutableStateOf<Bitmap?>(null)
+        }
+        val exoPlayer = remember {
+            ExoPlayer.Builder(context).build().apply {
+                repeatMode = REPEAT_MODE_ONE
+                volume = 0f
+                videoScalingMode = VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
+            }
+        }
+        exoPlayer.addListener(object : Listener {
+            override fun onPlaybackStateChanged(playbackState: Int) {
+                super.onPlaybackStateChanged(playbackState)
+                currentlyPlaying = playbackState == Player.STATE_READY
+            }
+        })
+        LaunchedEffect(key1 = videoUrl) {
+            val dataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory()
+            val mediaSource: MediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
+                .createMediaSource(MediaItem.fromUri(videoUrl))
+            val clip = ClippingMediaSource(mediaSource, clipStartPosition, clipEndPosition)
+            exoPlayer.setMediaSource(clip)
+            exoPlayer.prepare()
+            exoPlayer.playWhenReady = true
+            exoPlayer.addListener(object : Listener {
+                override fun onPlaybackStateChanged(playbackState: Int) {
+                    super.onPlaybackStateChanged(playbackState)
+                    isBuffering = playbackState == Player.STATE_BUFFERING
+                }
+            })
+        }
+        val lifecycleOwner by rememberUpdatedState(LocalLifecycleOwner.current)
+        DisposableEffect(lifecycleOwner) {
+            val lifecycle = lifecycleOwner.lifecycle
+            val observer = LifecycleEventObserver { _, event ->
+                when (event) {
+                    Lifecycle.Event.ON_PAUSE -> {
+                        exoPlayer.playWhenReady = false
+                    }
+                    Lifecycle.Event.ON_RESUME -> {
+                        exoPlayer.playWhenReady = true
+                    }
+                    Lifecycle.Event.ON_DESTROY -> {
+                        exoPlayer.run {
+                            stop()
+                            release()
+                        }
+                    }
+                    else -> {}
+                }
+            }
+            lifecycle.addObserver(observer)
+            onDispose {
+                lifecycle.removeObserver(observer)
+            }
+        }
+        Crossfade(targetState = hasFocus, animationSpec = tween(durationMillis = 700)) { focused ->
+            if (focused) {
+                Box {
+                    AndroidView(
+                        modifier = Modifier
+                            .fillMaxSize(),
+                        factory = {
+                            PlayerView(context).apply {
+                                player = exoPlayer
+                                useController = false
+                                resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
+                                controllerAutoShow = false
+                                setKeepContentOnPlayerReset(true)
+                                setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS)
+                            }
+                        }
+                    )
+                    if (isBuffering) {
+                        PreviewThumbnail(
+                            thumbnailUrl,
+                            thumbnailBitmap,
+                            thumbnailFrame,
+                            videoUrl,
+                            true
+                        ) {
+                            thumbnailBitmap = it
+                        }
+                    }
+                }
+            } else {
+                PreviewThumbnail(thumbnailUrl, thumbnailBitmap, thumbnailFrame, videoUrl, false) {
+                    thumbnailBitmap = it
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun PreviewThumbnail(
+    thumbnail: String?,
+    thumbnailBitmap: Bitmap?,
+    thumbnailFrame: Long,
+    videoUrl: String,
+    isLoadingVideo: Boolean,
+    onBitmapLoaded: (bitmap: Bitmap?) -> Unit
+) {
+    if (thumbnail == null && thumbnailBitmap == null) {
+        LaunchedEffect(videoUrl) {
+            withContext(Dispatchers.IO) {
+                MediaMetadataRetriever().apply {
+                    setDataSource(videoUrl, HashMap())
+                }.also {
+                    val result = it.getFrameAtTime(
+                        thumbnailFrame,
+                        MediaMetadataRetriever.OPTION_CLOSEST_SYNC
+                    )
+                    onBitmapLoaded(result)
+                    it.release()
+                }
+            }
+        }
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(Color.Black)
+                .border(2.dp, Color.White.copy(alpha = 0.4f)),
+            contentAlignment = Alignment.Center
+        ) {
+//            CircularProgressIndicator()
+        }
+    } else {
+        Box(
+            modifier = Modifier
+                .fillMaxSize(),
+            contentAlignment = Alignment.Center
+        ) {
+            if (thumbnailBitmap != null) {
+                Image(
+                    painter = rememberAsyncImagePainter(thumbnailBitmap),
+                    contentDescription = null,
+                    contentScale = ContentScale.Crop,
+                    alignment = Alignment.Center,
+                    modifier = Modifier.fillMaxSize()
+                )
+            } else {
+                AsyncImage(
+                    model = thumbnail,
+                    contentDescription = null,
+                    contentScale = ContentScale.Crop,
+                    alignment = Alignment.Center,
+                    modifier = Modifier.fillMaxSize()
+                )
+            }
+            if (isLoadingVideo) {
+//                CircularProgressIndicator(strokeWidth = 4.dp)
+            }
+        }
+    }
+}

+ 8 - 13
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/BigCarousel.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/carousel/BigCarousel.kt

@@ -6,7 +6,7 @@
  * w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.carousel
 
 import android.annotation.SuppressLint
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -23,7 +23,6 @@ import androidx.tv.material3.CardScale
 import androidx.tv.material3.Carousel
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvapp.base.ui.components.card.CarouselCard
-import org.mjdev.tvapp.data.Movie
 
 // todo swipe left and swipe right
 @SuppressLint("AutoboxingStateValueProperty")
@@ -32,24 +31,22 @@ import org.mjdev.tvapp.data.Movie
 @Composable
 fun BigCarousel(
     modifier: Modifier = Modifier,
-    items: List<Movie> = emptyList(),
+    items: List<Any?> = emptyList(),
     height: Dp = 260.dp,
-    onItemSelected: (movie: Movie?) -> Unit = {},
-    onItemClicked: (movie: Movie?) -> Unit = {},
+    onItemSelected: (movie: Any?) -> Unit = {},
+    onItemClicked: (movie: Any?) -> Unit = {},
 ) {
-
     val itemIndex = remember { mutableIntStateOf(0) }
-    val selectedMovie: () -> Movie? = { items[itemIndex.value] }
-
+    val selectedItem: () -> Any? = {
+        items[itemIndex.value]
+    }
     Carousel(
         itemCount = items.size,
         modifier = modifier
             .fillMaxWidth()
             .height(height),
     ) { indexOfCarouselItem ->
-
         itemIndex.value = indexOfCarouselItem
-
         CarouselCard(
             modifier = modifier
                 .fillMaxWidth()
@@ -58,10 +55,8 @@ fun BigCarousel(
             scale = CardScale.None,
             item = items[indexOfCarouselItem],
             onClick = {
-                onItemClicked(selectedMovie())
+                onItemClicked(selectedItem())
             }
         )
-
     }
-
 }

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

@@ -9,7 +9,6 @@
 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
@@ -26,7 +25,6 @@ import androidx.tv.material3.ClickableSurfaceDefaults
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Surface
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
-import org.mjdev.tvapp.base.extensions.ModifierExt.rememberMutableInteractionSource
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -46,7 +44,6 @@ fun FocusableBox(
     shape: Shape = RoundedCornerShape(roundCornerSize),
     borderColor: Color = Color.Transparent,
     borderSize: Dp = 0.dp,
-    interactionSource: MutableInteractionSource = rememberMutableInteractionSource(),
     focusState: MutableState<FocusState?> = rememberFocusState(),
     content: @Composable BoxScope.() -> Unit = {}
 ) {
@@ -88,7 +85,6 @@ fun FocusableBox(
             shape = shape
         ).let { b -> ClickableSurfaceDefaults.border(b, b, b, b, b) },
         glow = ClickableSurfaceDefaults.glow(),
-        interactionSource = interactionSource,
         content = content
     )
 

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

@@ -9,13 +9,9 @@
 package org.mjdev.tvapp.base.ui.components.complex
 
 import android.annotation.SuppressLint
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.onFocusChanged
@@ -31,7 +27,6 @@ import androidx.tv.material3.CardScale
 import androidx.tv.material3.CardShape
 import androidx.tv.material3.CompactCard
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import org.mjdev.tvapp.base.extensions.ModifierExt.isFocused
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
 import org.mjdev.tvapp.base.interfaces.ItemWithDescription
@@ -57,7 +52,6 @@ fun FocusableCard(
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
     placeholder: @Composable () -> Unit = {},
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     focusState: MutableState<FocusState?> = rememberFocusState(),
     imageRenderer: @Composable (modifier: Modifier) -> Unit = {
         ImageAny(
@@ -71,11 +65,7 @@ fun FocusableCard(
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
 ) {
-    val focusState = rememberSaveable {
-        mutableStateOf<FocusState?>(null)
-    }
     CompactCard(
-        interactionSource = interactionSource,
         scale = scale,
         shape = shape,
         colors = colors,

+ 10 - 10
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/NavDrawerContent.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/NavDrawerContent.kt

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.navigation
 
 import android.annotation.SuppressLint
 import androidx.compose.foundation.background
@@ -24,11 +24,11 @@ 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.MenuItem
-import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
-import org.mjdev.tvapp.base.ui.components.navigation.NavigationState
-import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.open
-import org.mjdev.tvapp.base.ui.components.navigation.rememberNavigationState
+import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.navigation.NavigationState
+import org.mjdev.tvapp.base.navigation.Screen.Companion.open
+import org.mjdev.tvapp.base.navigation.rememberNavigationState
 
 @SuppressLint("AutoboxingStateValueProperty")
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -63,7 +63,7 @@ fun NavDrawerContent(
     },
 ) {
 
-    val focused = remember { mutableIntStateOf(-1) }
+    val focusedIdx = remember { mutableIntStateOf(-1) }
 
     val topItems = navController.menuItems.filter { mi ->
         mi.menuGravity == MenuItem.Gravity.Top
@@ -97,7 +97,7 @@ fun NavDrawerContent(
                     text = menuItem.menuText,
                     onFocus = { id ->
                         navigationState.openDrawer()
-                        focused.value = id
+                        focusedIdx.value = id
                         onDrawerItemFocus(id)
                     },
                     onClick = { id ->
@@ -122,7 +122,7 @@ fun NavDrawerContent(
                     text = menuItem.menuText,
                     onFocus = { id ->
                         navigationState.openDrawer()
-                        focused.value = id
+                        focusedIdx.value = id
                         onDrawerItemFocus(id)
                     },
                     onClick = { id ->
@@ -147,7 +147,7 @@ fun NavDrawerContent(
                     icon = menuItem.menuIcon,
                     onFocus = { id ->
                         navigationState.openDrawer()
-                        focused.value = id
+                        focusedIdx.value = id
                         onDrawerItemFocus(id)
                     },
                     onClick = { id ->

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

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.navigation
 
 import androidx.compose.animation.AnimatedContentTransitionScope
 import androidx.compose.animation.EnterTransition
@@ -23,9 +23,11 @@ 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.ui.components.navigation.NavGraphBuilderEx
-import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
-import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.screen
+import org.mjdev.tvapp.base.navigation.NavGraphBuilderEx
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.navigation.Screen.Companion.screen
+import org.mjdev.tvapp.base.ui.components.screen.EmptyScreen
+import org.mjdev.tvapp.base.ui.components.screen.ScreenView
 
 @TvPreview
 @Composable

+ 38 - 24
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Navigation.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/navigation/Navigation.kt

@@ -6,11 +6,12 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.complex
+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.Box
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
@@ -21,16 +22,15 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.ModalNavigationDrawer
 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.extensions.NavExt.rememberNavControllerEx
-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.navigation.NavigationState
-import org.mjdev.tvapp.base.ui.components.navigation.rememberNavigationState
+import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.navigation.NavigationState
+import org.mjdev.tvapp.base.navigation.rememberNavigationState
 
 @SuppressLint("AutoboxingStateValueProperty")
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -61,28 +61,42 @@ fun Navigation(
     navController.addMenuItem(*menuItems.toTypedArray())
 
     if (navController.menuState.value) {
-// todo right navigation with settings
-//        ModalNavigationDrawer(
-//            drawerContent = {}) {
-        NavigationDrawer(
+        SettingsDrawer(
+            drawerState = navController.settingsDrawerState,
             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)
-            }
-        )
-//        }
+                .border(borderSize, borderColor, shape)
+        ) {
+            NavigationDrawer(
+                modifier = modifier
+                    .fillMaxHeight()
+                    .background(backgroundColor, shape)
+                    .border(borderSize, borderColor, shape),
+                content = {
+                    Box(
+                        modifier.fillMaxHeight()
+                    ) {
+                        content()
+                    }
+                },
+                drawerState = navigationState.drawerState,
+                drawerContent = { state ->
+                    NavDrawerContent(
+                        backgroundColor = backgroundColor,
+                        navController = navController,
+                        navigationState = navigationState,
+                    )
+                    navigationState.drawerState.setValue(state)
+                }
+            )
+        }
     } else {
-        content()
+        Box(
+            modifier.fillMaxHeight()
+        ) {
+            content()
+        }
     }
 
 }

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

@@ -6,25 +6,26 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.navigation
 
 import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.background
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AccountCircle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
@@ -34,8 +35,9 @@ 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.extensions.ModifierExt
+import org.mjdev.tvapp.base.extensions.ModifierExt.isFocused
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
+import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.icon.IconAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
@@ -45,7 +47,6 @@ import org.mjdev.tvapp.base.ui.components.text.TextAny
 fun NavigationRow(
     modifier: Modifier = Modifier,
     id: Int = -1,
-    drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Open),
     text: Any? = "menu item 1",
     icon: Any? = Icons.Default.AccountCircle,
     textColor: Color = Color.White,
@@ -53,24 +54,44 @@ fun NavigationRow(
     focusedColor: Color = Color.Green,
     unFocusedColor: Color = Color.Transparent,
     expandedWidth: Dp = 150.dp,
-    contentPadding: Dp = 2.dp,
+    roundCornerSize: Dp = 32.dp,
+    shape: Shape = RoundedCornerShape(roundCornerSize),
+    strokeWidth: Dp = 2.dp,
+    margin: Dp = 2.dp,
+    padding: Dp = 4.dp,
+//    brush: Brush? = BrushBuilder()
+//        .type(BrushBuilder.Vertical)
+//        .colors(
+//            0.1f to Color.White,
+//            0.3f to Color.Green,
+//            0.4f to Color.Green,
+//            0.5f to Color.Green,
+//            0.9f to Color.Green,
+//        )
+//        .build(),
+    drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Open),
     focusState: MutableState<FocusState?> = rememberFocusState(),
     onFocus: (id: Int) -> Unit = {},
     onClick: (id: Int) -> Unit = {},
 ) {
 
     val isEdit = isEditMode()
-    val focused = remember { mutableStateOf(false) }
 
     FocusableBox(
         modifier = modifier
-            .padding(contentPadding)
-            .background(
-                if (isEdit || focused.value) focusedColor else unFocusedColor
+            .padding(margin)
+            .border(
+                BorderStroke(
+                    strokeWidth,
+                    if (isEdit || focusState.isFocused) focusedColor
+                    else unFocusedColor
+                ),
+                shape
             ),
         focusState = focusState,
-        focusedColor = focusedColor,
-        unFocusedColor = unFocusedColor,
+        shape = shape,
+        focusedColor = focusedColor.copy(alpha = 0.5f),
+        unFocusedColor = Color.Transparent,
         onFocus = {
             onFocus(id)
         },
@@ -79,6 +100,9 @@ fun NavigationRow(
         },
     ) {
         Row(
+            modifier = Modifier
+                .padding(padding),
+//                .background(brush!!, shape),
             horizontalArrangement = Arrangement.Start,
             verticalAlignment = Alignment.CenterVertically,
         ) {

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

@@ -0,0 +1,37 @@
+/*
+ * 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.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.tv.material3.DrawerState
+import androidx.tv.material3.DrawerValue
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.ModalNavigationDrawer
+import androidx.tv.material3.rememberDrawerState
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun SettingsDrawer(
+    modifier: Modifier = Modifier,
+    drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
+    scrimColor: Color = MaterialTheme.colorScheme.scrim.copy(alpha = 0.5f),
+    content: @Composable () -> Unit = {}
+) = ModalNavigationDrawer(
+    modifier = modifier,
+    drawerState = drawerState,
+    scrimColor = scrimColor,
+    content = content,
+    drawerContent = {
+        // todo
+    },
+
+    )

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

@@ -25,10 +25,9 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
 import org.mjdev.tvapp.base.ui.components.complex.TouchBox
-import org.mjdev.tvapp.base.ui.components.navigation.MenuItem
-import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 @Suppress("LeakingThis")

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

@@ -13,8 +13,8 @@ import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableIntStateOf
 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
+import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 
 class PagerScope(
     val navController: NavHostControllerEx,

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

@@ -14,7 +14,7 @@ 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
+import org.mjdev.tvapp.base.navigation.MenuItem
 
 open class SearchPage : Page() {
 

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

@@ -14,7 +14,7 @@ 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
+import org.mjdev.tvapp.base.navigation.MenuItem
 
 open class SettingsPage : Page() {
 

+ 3 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/TvPager.kt

@@ -26,9 +26,9 @@ import kotlinx.coroutines.launch
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
 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.navigation.MenuItemClickListener
+import org.mjdev.tvapp.base.navigation.MenuItem
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
 import org.mjdev.tvapp.base.ui.components.page.Page.Companion.EMPTY_PAGE
 
 @SuppressLint("AutoboxingStateValueProperty")

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

@@ -6,11 +6,11 @@
  *  w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.screen
 
 import androidx.compose.runtime.Composable
 import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.ui.components.navigation.Screen
+import org.mjdev.tvapp.base.navigation.Screen
 
 internal class EmptyScreen : Screen() {
 

+ 7 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScreenView.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/screen/ScreenView.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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.screen
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -15,7 +15,8 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.NavExt.rememberNavControllerEx
-import org.mjdev.tvapp.base.ui.components.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.navigation.NavHostControllerEx
+import org.mjdev.tvapp.base.ui.components.navigation.Navigation
 
 @Suppress("TrailingComma", "unused")
 @TvPreview

+ 57 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/text/TextWithShadow.kt

@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.base.ui.components.text
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material3.MaterialTheme
+
+@Preview
+@Composable
+fun TextWithShadow(
+    modifier: Modifier = Modifier,
+    shadowSize: Dp = 8.dp,
+    shadowColor: Color = Color.Green,
+    offsetX: Dp = 0.dp,
+    offsetY: Dp = 0.dp,
+    fontWeight: FontWeight = FontWeight.Normal,
+    fontSize: TextUnit = 16.sp,
+    color: Color = Color.White,
+    text: Any? = "test"
+) {
+    TextAny(
+        modifier = modifier.padding(
+            shadowSize,
+            shadowSize,
+            shadowSize,
+            shadowSize
+        ),
+        style = MaterialTheme.typography.bodyMedium.copy(
+            shadow = Shadow(
+                color = shadowColor,
+                offset = Offset(offsetX.value, offsetY.value),
+                blurRadius = shadowSize.value
+            )
+        ),
+        text = text,
+        fontWeight = fontWeight,
+        fontSize = fontSize,
+        color = color
+    )
+}

+ 105 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/BrowseView.kt

@@ -0,0 +1,105 @@
+/*
+ * 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.tv
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+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.StringExt.asException
+import org.mjdev.tvapp.base.interfaces.ItemWithTitle
+import org.mjdev.tvapp.base.network.NetworkStatus
+import org.mjdev.tvapp.base.network.isNotConnected
+import org.mjdev.tvapp.base.ui.components.carousel.BigCarousel
+
+@TvPreview
+@Composable
+fun BrowseView(
+    modifier: Modifier = Modifier,
+    showHeader: Boolean = true,
+    showNetworkState: Boolean = true,
+    title: Any? = "test",
+    messages: List<Any?> = emptyList(),
+    categories: List<Any?> = emptyList(),
+    featuredItems: List<Any?> = emptyList(),
+    categoriesAndItemsMap: Map<*, List<*>> = emptyMap<Any?, List<Any?>>(),
+    networkState: State<NetworkStatus?> = mutableStateOf(NetworkStatus.Unknown),
+    errorState: State<Throwable?> = mutableStateOf(null),
+    backgroundColor: Color = Color.DarkGray,
+    roundCornerSize: Dp = 0.dp,
+    backgroundShape: Shape = RoundedCornerShape(roundCornerSize),
+    verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(32.dp),
+    contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
+    onItemClick: (item: Any?) -> Unit = {}
+) {
+    val isEdit = isEditMode()
+    ScrollableTvLazyRow(
+        modifier = Modifier
+            .fillMaxSize()
+            .background(backgroundColor, backgroundShape),
+        verticalArrangement = verticalArrangement,
+        contentPadding = contentPadding
+    ) {
+        if (isEdit || showHeader) item {
+            Header(
+                title = title,
+                messagesCount = messages.size
+            )
+        }
+        if (showNetworkState && (isEdit || networkState.isNotConnected)) item {
+            ErrorMessage(
+                error = stringResource(R.string.error_no_network).asException(),
+                backgroundColor = Color.Black,
+                dismissible = false
+            )
+        }
+        if (isEdit || (errorState.value is Exception)) item {
+            ErrorMessage(
+                error = errorState.value,
+                dismissible = false
+            )
+        }
+        if (isEdit || categories.isNotEmpty()) item {
+            Tabs(
+                items = categories.map { category ->
+                    (category as ItemWithTitle).title
+                }
+            )
+        }
+        if (isEdit || featuredItems.isNotEmpty()) item {
+            BigCarousel(
+                modifier = Modifier,
+                items = featuredItems,
+                onItemClicked = onItemClick
+            )
+        }
+        items(categoriesAndItemsMap.map {
+            Pair(it.key, it.value)
+        }) { entry ->
+            CategoryRow(
+                title = (entry.first as? ItemWithTitle)?.title,
+                items = entry.second,
+                onItemClick = onItemClick
+            )
+        }
+    }
+}

+ 58 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/CategoryRow.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.tv
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyListState
+import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
+import org.mjdev.tvapp.base.ui.components.card.PhotoCard
+import org.mjdev.tvapp.base.ui.components.text.TextAny
+
+@Suppress("UNUSED_PARAMETER")
+@OptIn(ExperimentalTvMaterial3Api::class)
+@Composable
+fun CategoryRow(
+    title: Any? = "category 1",
+    items: List<Any?> = listOf(),
+    rowState: TvLazyListState = rememberTvLazyListState(),
+    onItemClick: (item: Any?) -> Unit = {},
+) {
+    val focusState = rememberFocusState()
+    TextAny(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(0.dp, 16.dp),
+        text = title,
+        color = Color.White,
+        fontWeight = FontWeight.Bold
+    )
+    TVRow(
+        horizontalArrangement = Arrangement.spacedBy(16.dp),
+        modifier = Modifier.height(200.dp),
+        contentPadding = 16.dp,
+    ) {
+        items(items) { item ->
+            PhotoCard(
+                focusState = focusState,
+                item = item,
+                onClick = onItemClick,
+            )
+        }
+    }
+}

+ 6 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Clock.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Clock.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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.tv
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
@@ -35,8 +35,8 @@ import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.launch
 import org.mjdev.tvapp.base.extensions.ContextExt.dateAsString
 import org.mjdev.tvapp.base.extensions.ContextExt.timeAsString
-import org.mjdev.tvapp.base.extensions.ModifierExt
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
+import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 @Preview

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

@@ -6,7 +6,7 @@
  * w: https://mjdev.org
  */
 
-package org.mjdev.tvapp.base.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.tv
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box

+ 6 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Header.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Header.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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.tv
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -27,6 +27,7 @@ import androidx.compose.ui.unit.sp
 import androidx.constraintlayout.compose.ConstraintLayout
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.badge.Badge
 
 @TvPreview
 @Composable

+ 6 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/ScrollableTvLazyRow.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/ScrollableTvLazyRow.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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.tv
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
@@ -20,6 +20,7 @@ import androidx.tv.foundation.lazy.list.TvLazyListScope
 import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvapp.base.annotations.TvPreview
+import org.mjdev.tvapp.base.ui.components.complex.TouchBox
 
 @TvPreview
 @Composable

+ 62 - 0
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/TVRow.kt

@@ -0,0 +1,62 @@
+/*
+ * 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.tv
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.PivotOffsets
+import androidx.tv.foundation.lazy.list.TvLazyListItemScope
+import androidx.tv.foundation.lazy.list.TvLazyListScope
+import androidx.tv.foundation.lazy.list.TvLazyListState
+import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+
+@Composable
+fun TVRow(
+    modifier: Modifier = Modifier,
+    state: TvLazyListState = rememberTvLazyListState(),
+    contentPadding: Dp = 0.dp,
+    reverseLayout: Boolean = false,
+    horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start
+    else Arrangement.End,
+    verticalAlignment: Alignment.Vertical = Alignment.Top,
+    userScrollEnabled: Boolean = true,
+    pivotOffsets: PivotOffsets = PivotOffsets(),
+    content: TvLazyListScope.() -> Unit
+) {
+    TvLazyRow(
+        modifier = modifier,
+        state = state,
+        contentPadding = PaddingValues(contentPadding),
+        reverseLayout = reverseLayout,
+        horizontalArrangement = horizontalArrangement,
+        verticalAlignment = verticalAlignment,
+        userScrollEnabled = userScrollEnabled,
+        pivotOffsets = pivotOffsets,
+        content = content
+    )
+}
+
+inline fun <T> TvLazyListScope.items(
+    items: List<T>,
+    noinline key: ((item: T) -> Any)? = null,
+    noinline contentType: (item: T) -> Any? = { null },
+    crossinline itemContent: @Composable TvLazyListItemScope.(item: T) -> Unit
+) = items(
+    count = items.size,
+    key = if (key != null) { index: Int -> key(items[index]) } else null,
+    contentType = { index: Int -> contentType(items[index]) }
+) {
+    itemContent(items[it])
+}

+ 6 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Tabs.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Tabs.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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.tv
 
 import android.annotation.SuppressLint
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -32,6 +32,7 @@ import androidx.tv.material3.TabRowDefaults
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberMutableInteractionSource
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
+import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
 @SuppressLint("AutoboxingStateValueProperty")

+ 12 - 27
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Title.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/Title.kt

@@ -1,14 +1,13 @@
 /*
  * 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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.tv
 
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.height
@@ -20,23 +19,20 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.scale
 import androidx.compose.ui.focus.FocusState
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import androidx.tv.material3.MaterialTheme
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
-import org.mjdev.tvapp.base.extensions.ModifierExt
 import org.mjdev.tvapp.base.extensions.ModifierExt.isFocused
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
+import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.image.CircleImage
-import org.mjdev.tvapp.base.ui.components.text.TextAny
+import org.mjdev.tvapp.base.ui.components.text.TextWithShadow
 
 @Preview
 @Composable
@@ -50,7 +46,6 @@ fun Title(
     shadowSize: Dp = 6.dp,
     title: Any? = R.string.app_name,
     focusState: MutableState<FocusState?> = rememberFocusState(),
-    interactionSource: MutableInteractionSource = ModifierExt.rememberMutableInteractionSource(),
     onFocus: () -> Unit = {},
     onClick: () -> Unit = {},
 ) {
@@ -63,7 +58,6 @@ fun Title(
         onClick = onClick,
         shape = RoundedCornerShape(8.dp),
         focusedColor = Color.Transparent,
-        interactionSource = interactionSource,
     ) {
         Row(
             modifier = modifier.padding(8.dp, 8.dp, 8.dp, 8.dp),
@@ -78,20 +72,11 @@ fun Title(
                 borderSize = 1.dp,
                 src = R.mipmap.ic_launcher
             )
-            TextAny(
-                modifier = modifier.padding(
-                    10.dp,
-                    shadowSize,
-                    shadowSize,
-                    shadowSize
-                ),
-                style = MaterialTheme.typography.bodyMedium.copy(
-                    shadow = Shadow(
-                        color = if (isEdit || focusState.isFocused) focusShadowColor else unfocusedShadowColor,
-                        offset = Offset(0f, 0f),
-                        blurRadius = shadowSize.value
-                    )
-                ),
+            TextWithShadow(
+                modifier = modifier,
+                shadowColor = if (isEdit || focusState.isFocused) focusShadowColor
+                else unfocusedShadowColor,
+                shadowSize = 10.dp,
                 text = title ?: R.string.app_name,
                 fontWeight = fontWeight,
                 fontSize = fontSize,

+ 6 - 6
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/UserPic.kt → app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/UserPic.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.ui.components.complex
+package org.mjdev.tvapp.base.ui.components.tv
 
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.shape.CircleShape
@@ -22,8 +22,8 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
 import org.mjdev.tvapp.R
-import org.mjdev.tvapp.base.extensions.ModifierExt
 import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
+import org.mjdev.tvapp.base.ui.components.complex.FocusableBox
 import org.mjdev.tvapp.base.ui.components.image.CircleImage
 
 @OptIn(ExperimentalTvMaterial3Api::class)

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

@@ -9,13 +9,7 @@
 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
 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
@@ -25,29 +19,15 @@ 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.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.foundation.lazy.list.items
-import androidx.tv.material3.ExperimentalTvMaterial3Api
 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.interfaces.ItemWithId
 import org.mjdev.tvapp.base.interfaces.ItemWithVideoUri
-import org.mjdev.tvapp.base.ui.components.navigation.Screen.Companion.open
-import org.mjdev.tvapp.base.network.NetworkStatus
-import org.mjdev.tvapp.base.ui.components.card.PhotoCard
+import org.mjdev.tvapp.base.navigation.Screen.Companion.open
 import org.mjdev.tvapp.base.ui.components.page.Page
-import org.mjdev.tvapp.base.ui.components.complex.BigCarousel
-import org.mjdev.tvapp.base.ui.components.complex.ScrollableTvLazyRow
-import org.mjdev.tvapp.base.ui.components.complex.Tabs
-import org.mjdev.tvapp.base.ui.components.complex.ErrorMessage
-import org.mjdev.tvapp.base.ui.components.complex.Header
-import org.mjdev.tvapp.base.ui.components.text.TextAny
+import org.mjdev.tvapp.base.ui.components.tv.BrowseView
 import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.PlayerScreen
 import org.mjdev.tvapp.viewmodel.MainViewModel
@@ -58,12 +38,9 @@ class MainPage : Page() {
     override val title: Int = R.string.title_home
     override val icon: ImageVector = Icons.Default.Home
 
-    @OptIn(ExperimentalTvMaterial3Api::class)
     @TvPreview
     @Composable
     override fun Content() {
-        val isEdit = isEditMode()
-
         val viewModel: MainViewModel = appViewModel { context ->
             MainViewModel.mockMainViewModel(context)
         }
@@ -92,72 +69,18 @@ class MainPage : Page() {
             errorState.value = error
         }
 
-        ScrollableTvLazyRow(
-            modifier = Modifier
-                .fillMaxSize()
-                .background(backgroundColor, background),
-            verticalArrangement = Arrangement.spacedBy(32.dp),
-            contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
-        ) {
-            item {
-                Header(
-                    title = titleState.value,
-                    messagesCount = messages.value.size
-                )
-            }
-            if (isEdit || (networkState.value !is NetworkStatus.Connected)) item {
-                ErrorMessage(
-                    error = stringResource(R.string.error_no_network).asException(),
-                    backgroundColor = Color.Black,
-                    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 { category -> category.title }
-                )
-            }
-            if (isEdit || featuredMovieList.value.isNotEmpty()) item {
-                BigCarousel(
-                    modifier = Modifier,
-                    items = featuredMovieList.value,
-                    onItemClicked = onItemClick
-                )
-            }
-            items(categoryList.value) { category ->
-                TextAny(
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .padding(0.dp, 16.dp),
-                    text = category.title,
-                    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 ->
-                        PhotoCard(
-                            item = item,
-                            onClick = onItemClick
-                        )
-                    }
-                }
-            }
-        }
+        BrowseView(
+            modifier = Modifier.fillMaxSize(),
+            title = titleState.value,
+            messages = messages.value,
+            categories = categoryList.value,
+            featuredItems = featuredMovieList.value,
+            categoriesAndItemsMap = categoryList.value.associateWith { it.movieList },
+            networkState = networkState,
+            errorState = errorState,
+            backgroundColor = Color.DarkGray,
+            onItemClick = onItemClick
+        )
 
     }
 

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

@@ -20,7 +20,7 @@ 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.ui.components.navigation.Screen
+import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.state.DetailsLoadingState
 import org.mjdev.tvapp.ui.components.Details
 import org.mjdev.tvapp.ui.components.Loading

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

@@ -14,7 +14,7 @@ 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.ui.components.navigation.ScreenWithPages
+import org.mjdev.tvapp.base.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

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

@@ -23,7 +23,7 @@ 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.ui.components.navigation.Screen
+import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.state.DetailsLoadingState
 import org.mjdev.tvapp.ui.components.Loading
 import org.mjdev.tvapp.ui.components.MediaPlayer

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

@@ -16,27 +16,25 @@ 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
+import androidx.compose.material.icons.filled.Tv
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.scale
-import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import androidx.tv.material3.MaterialTheme
 import kotlinx.coroutines.delay
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.isEditMode
-import org.mjdev.tvapp.base.ui.components.navigation.Screen
+import org.mjdev.tvapp.base.navigation.Screen
 import org.mjdev.tvapp.base.ui.components.icon.IconAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
+import org.mjdev.tvapp.base.ui.components.text.TextWithShadow
 
 class SplashScreen : Screen() {
 
@@ -59,22 +57,17 @@ class SplashScreen : Screen() {
         ) {
             IconAny(
                 modifier = Modifier.size(128.dp),
-                src = Icons.Sharp.Tv,
-                tint = Color.White.copy(alpha = 0.5f)
+                src = Icons.Default.Tv,
+                tint=Color.Black
             )
-            TextAny(
+            TextWithShadow(
                 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
-                    )
-                ),
+                shadowColor = Color.Green,
+                shadowSize = 18.dp
             )
         }
         Box(
@@ -90,7 +83,7 @@ class SplashScreen : Screen() {
         ) {
             TextAny(
                 text = R.string.author,
-                fontSize = 14.sp,
+                fontSize = 12.sp,
                 fontWeight = FontWeight.Bold,
                 color = Color.White.copy(alpha = 0.5f)
             )