Browse Source

design improvements

design improvements

design improvements

design improvements

design improvements

design improvements
Milan Jurkulak 3 years ago
parent
commit
f03f6643b6
33 changed files with 438 additions and 358 deletions
  1. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  2. 1 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/SplashScreen.kt
  3. 0 2
      tvlib/build.gradle.kts
  4. 12 7
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt
  5. 5 19
      tvlib/src/main/kotlin/org/mjdev/tvlib/screen/Screen.kt
  6. 4 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/screen/ScreenWithPages.kt
  7. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/FocusHelper.kt
  8. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/ItemCard.kt
  9. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/PhotoCard.kt
  10. 7 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/BigCarousel.kt
  11. 13 13
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/Carousel.kt
  12. 2 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/CarouselCard.kt
  13. 3 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/complex/FocusableBox.kt
  14. 2 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/complex/FocusableCard.kt
  15. 25 11
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt
  16. 3 10
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/ItemInfo.kt
  17. 21 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/ImageBackground.kt
  18. 19 20
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/DrawerSheet.kt
  19. 32 18
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/ModalNavigationDrawer.kt
  20. 115 69
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavDrawerContent.kt
  21. 11 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavHostEx.kt
  22. 11 25
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/Navigation.kt
  23. 0 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationDrawer.kt
  24. 33 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationRow.kt
  25. 2 15
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt
  26. 1 16
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawerContent.kt
  27. 13 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/Page.kt
  28. 9 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/PagerScope.kt
  29. 3 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/TvPager.kt
  30. 1 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/screen/ScreenView.kt
  31. 65 95
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/ErrorMessage.kt
  32. 18 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Header.kt
  33. 3 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Title.kt

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

@@ -116,7 +116,7 @@ class MainPage : Page() {
                 onItemClicked = onItemClick,
                 onUserPicClicked = {
                     if (authManager.isUserLoggedIn) {
-                        // todo show details / settings
+                        navController?.openSettings()
                     } else {
                         authManager.login()
                     }

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

@@ -44,7 +44,7 @@ import org.mjdev.tvlib.ui.components.text.TextAny
 class SplashScreen : Screen() {
 
     override val showOnce: Boolean = true
-    override val backgroundColor: Color = Color.Black
+    override val backgroundColor: Color = Color.Transparent
     override val immersive: Boolean = true
 
     @OptIn(ExperimentalPermissionsApi::class)

+ 0 - 2
tvlib/build.gradle.kts

@@ -111,8 +111,6 @@ dependencies {
     implementation("io.coil-kt:coil-video:2.4.0")
     implementation("io.coil-kt:coil-compose:2.4.0")
     implementation("com.github.skydoves:landscapist-coil:2.2.5")
-    // constraint layout // todo remove
-    implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
     // debug
     implementation("com.jakewharton.timber:timber:5.0.1")
     // dagger core

+ 12 - 7
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt

@@ -81,18 +81,23 @@ object ModifierExt {
 
     fun Modifier.requestFocusOnTouch(
         focusRequester: FocusRequester,
+        requestFocus: Boolean = true,
         onTouch: () -> Unit = {}
     ): Modifier = this then focusRequester(
         focusRequester
     ).pointerInput(this) {
-        detectTapGestures {
-            try {
-                onTouch()
-                focusRequester.requestFocus()
-            } catch (e: Throwable) {
-                Timber.e(e)
+        detectTapGestures(
+            onTap = {
+                try {
+                    onTouch()
+                    if (requestFocus) {
+                        focusRequester.requestFocus()
+                    }
+                } catch (e: Throwable) {
+                    Timber.e(e)
+                }
             }
-        }
+        )
     }
 
     @Stable

+ 5 - 19
tvlib/src/main/kotlin/org/mjdev/tvlib/screen/Screen.kt

@@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -38,7 +37,6 @@ import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvlib.navigation.MenuItem
 import org.mjdev.tvlib.navigation.NavHostControllerEx
-import org.mjdev.tvlib.ui.components.image.FadingPhotoImage
 
 @Suppress("unused", "LeakingThis")
 open class Screen {
@@ -97,24 +95,12 @@ open class Screen {
         this.args = args
         navController.menuState.value = !immersive
         Box(
-            modifier = Modifier.fillMaxSize()
+            modifier = Modifier
+                .fillMaxSize()
+                .navigationBarsPadding()
+                .statusBarsPadding()
         ) {
-            FadingPhotoImage(
-                modifier = Modifier.fillMaxSize(),
-                fadingImageState = navController.backgroundState,
-                contrast = 4f,
-                alpha = 0.7f,
-                brightness = -255f,
-                contentScale = ContentScale.Crop
-            )
-            Box(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .navigationBarsPadding()
-                    .statusBarsPadding()
-            ) {
-                ComposeScreen()
-            }
+            ComposeScreen()
         }
     }
 

+ 4 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/screen/ScreenWithPages.kt

@@ -30,7 +30,10 @@ open class ScreenWithPages : Screen() {
         TvPager(
             navController = navController,
             startIndex = startPageIndex,
-            pages = pages
+            pages = pages,
+            onPageChange = { _, _ ->
+                // todo, page state & update menu index
+            }
         )
     }
 

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/CardFocus.kt → tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/FocusHelper.kt

@@ -10,7 +10,7 @@ package org.mjdev.tvlib.ui.components.card
 
 import androidx.compose.ui.focus.FocusState
 
-class CardFocus(
+class FocusHelper(
     private var focused: Boolean
 ) : FocusState {
     override val hasFocus: Boolean get() = focused

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/ItemCard.kt

@@ -65,7 +65,7 @@ fun ItemCard(
     focused: Boolean = isEditMode(),
     focusState: MutableState<FocusState?> = rememberFocusState(
         item,
-        CardFocus(focused)
+        FocusHelper(focused)
     ),
     focusRequester: FocusRequester = rememberFocusRequester(item),
     titlePadding: PaddingValues = PaddingValues(8.dp),

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/PhotoCard.kt

@@ -47,7 +47,7 @@ fun PhotoCard(
     focused: Boolean = ComposeExt.isEditMode(),
     focusState: MutableState<FocusState?> = rememberFocusState(
         item,
-        CardFocus(focused)
+        FocusHelper(focused)
     ),
     focusRequester: FocusRequester = rememberFocusRequester(item),
     @FloatRange(from = 0.0, to = 10.0)

+ 7 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/BigCarousel.kt

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.height
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalConfiguration
@@ -22,7 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
+import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 
@@ -42,26 +43,29 @@ fun BigCarousel(
         if (config.orientation == Configuration.ORIENTATION_PORTRAIT) config.screenWidthDp * 0.4f
         else config.screenHeightDp * 0.4f
     }
-    val focusState = rememberFocusState(items)
+    val focusRequester = rememberFocusRequester()
     Carousel(
         autoScrollDurationMillis = autoScrollDurationMillis,
         carouselState = carouselState,
         itemCount = items.size,
         modifier = modifier
             .recomposeHighlighter()
+            .focusRequester(focusRequester)
             .fillMaxWidth()
             .height(height.dp)
             .pointerInput(Unit) {
                 detectSwipe(
                     onSwipeLeft = {
                         carouselState.moveToNextItem(items.size)
+                        focusRequester.requestFocus()
                     },
                     onSwipeRight = {
                         carouselState.moveToPreviousItem(items.size)
+                        focusRequester.requestFocus()
                     },
                 )
             },
-    ) {indexOfCarouselItem ->
+    ) { indexOfCarouselItem ->
         val selectedItem = items[indexOfCarouselItem]
         onItemSelected(selectedItem)
         CarouselCard(
@@ -71,7 +75,6 @@ fun BigCarousel(
                 .height(height.dp),
             contentScale = ContentScale.Crop,
             scale = CardScale.None,
-            focusState = focusState,
             onFocus = onItemSelected,
             onClick = {
                 onItemClicked(selectedItem)

+ 13 - 13
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/Carousel.kt

@@ -20,15 +20,13 @@ import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
@@ -37,6 +35,8 @@ import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import org.mjdev.tvlib.extensions.ComposeExt.isFocused
+import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ModifierExt.bringIntoViewIfChildrenAreFocused
 import org.mjdev.tvlib.ui.components.carousel.CarouselDefaults.CarouselStateUpdater
 import org.mjdev.tvlib.ui.components.carousel.CarouselDefaults.onAnimationCompletion
@@ -64,16 +64,16 @@ fun Carousel(
                 .padding(16.dp),
         )
     },
+    isAutoScrollActive: MutableState<Boolean> = remember { mutableStateOf(false) },
+    focusState: MutableState<FocusState?> = remember { mutableStateOf(null) },
     content: @Composable AnimatedContentScope.(index: Int) -> Unit
 ) {
 
     CarouselStateUpdater(carouselState, itemCount)
 
-    var focusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
     val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
-    val carouselOuterBoxFocusRequester = remember { FocusRequester() }
-    var isAutoScrollActive by remember { mutableStateOf(false) }
+    val carouselOuterBoxFocusRequester = rememberFocusRequester()
     val context = LocalContext.current
 
     val accessibilityManager = remember {
@@ -84,9 +84,9 @@ fun Carousel(
         autoScrollDurationMillis = autoScrollDurationMillis,
         itemCount = itemCount,
         carouselState = carouselState,
-        doAutoScroll = shouldPerformAutoScroll(focusState, accessibilityManager),
+        doAutoScroll = shouldPerformAutoScroll(focusState.value, accessibilityManager),
         onAutoScrollChange = {
-            isAutoScrollActive = it
+            isAutoScrollActive.value = it
         }
     )
 
@@ -95,8 +95,8 @@ fun Carousel(
         .bringIntoViewIfChildrenAreFocused()
         .focusRequester(carouselOuterBoxFocusRequester)
         .onFocusChanged {
-            focusState = it
-            if (it.isFocused && isAutoScrollActive) {
+            focusState.value = it
+            if (it.isFocused && isAutoScrollActive.value) {
                 focusManager.moveFocus(FocusDirection.Enter)
             }
         }
@@ -107,7 +107,7 @@ fun Carousel(
             itemCount = itemCount,
             isLtr = isLtr
         ) {
-            focusState
+            focusState.value
         }
         .focusable()
     ) {
@@ -123,11 +123,11 @@ fun Carousel(
             label = "CarouselAnimation"
         ) { activeItemIndex ->
             LaunchedEffect(Unit) {
-                if (accessibilityManager.isEnabled) {
+                if (accessibilityManager.isEnabled && focusState.isFocused) {
                     carouselOuterBoxFocusRequester.requestFocus()
                 }
                 this@AnimatedContent.onAnimationCompletion {
-                    if (!isAutoScrollActive && focusState?.isFocused == true) {
+                    if ((!isAutoScrollActive.value) && focusState.isFocused) {
                         carouselOuterBoxFocusRequester.requestFocus()
                         focusManager.moveFocus(FocusDirection.Enter)
                     }

+ 2 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/CarouselCard.kt

@@ -28,7 +28,7 @@ import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
-import org.mjdev.tvlib.ui.components.card.CardFocus
+import org.mjdev.tvlib.ui.components.card.FocusHelper
 import org.mjdev.tvlib.ui.components.card.PhotoCard
 import org.mjdev.tvlib.ui.components.image.ImageAny
 
@@ -44,7 +44,7 @@ fun CarouselCard(
     focused: Boolean = isEditMode(),
     focusState: MutableState<FocusState?> = rememberFocusState(
         item,
-        CardFocus(focused)
+        FocusHelper(focused)
     ),
     imageRenderer: @Composable () -> Unit = {
         val image = (item as? ItemWithImage<*>)?.image

+ 3 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/complex/FocusableBox.kt

@@ -60,7 +60,9 @@ fun FocusableBox(
             .recomposeHighlighter()
             .focusState(focusState)
             .onFocusChanged { state -> onFocusChange(state) }
-            .requestFocusOnTouch(focusRequester) {
+            .requestFocusOnTouch(
+                focusRequester
+            ) {
                 if (focusState.isFocused) {
                     onClick()
                 }

+ 2 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/complex/FocusableCard.kt

@@ -51,7 +51,7 @@ import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithSubtitle
 import org.mjdev.tvlib.interfaces.ItemWithTitle
-import org.mjdev.tvlib.ui.components.card.CardFocus
+import org.mjdev.tvlib.ui.components.card.FocusHelper
 import org.mjdev.tvlib.ui.components.card.colorFocusBorder
 import org.mjdev.tvlib.ui.components.card.colorFocusGlow
 import org.mjdev.tvlib.ui.components.image.ImageAny
@@ -86,7 +86,7 @@ fun FocusableCard(
     focusRequester: FocusRequester = rememberFocusRequester(item),
     focusState: MutableState<FocusState?> = rememberFocusState(
         item,
-        CardFocus(focused)
+        FocusHelper(focused)
     ),
     onFocus: (item: Any?) -> Unit = {},
     onFocusChange: (state: FocusState) -> Unit = { state ->

+ 25 - 11
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt

@@ -10,7 +10,9 @@ package org.mjdev.tvlib.ui.components.gallery
 
 import android.view.Gravity
 import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_DPAD_CENTER
 import android.view.KeyEvent.KEYCODE_DPAD_DOWN
+import android.view.KeyEvent.KEYCODE_ENTER
 import android.view.KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.ExperimentalAnimationApi
@@ -54,6 +56,7 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import kotlinx.coroutines.delay
 import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.GlobalExt.toggle
 import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
 import org.mjdev.tvlib.interfaces.ItemPhoto
@@ -101,7 +104,8 @@ fun Gallery(
     },
     infoVisible: MutableState<Boolean> = remember { mutableStateOf(true) },
     currentItemIndex: MutableIntState = remember { mutableIntStateOf(index) },
-    focusRequester: FocusRequester = FocusRequester(),
+    focusRequester: FocusRequester = rememberFocusRequester(),
+    imageScaleType: MutableState<ContentScale> = remember { mutableStateOf(ContentScale.Crop) },
     imageFromItem: (index: Int) -> Any? = { idx ->
         list[idx].let { data ->
             val photo = (data as? ItemPhoto)?.uri
@@ -110,11 +114,21 @@ fun Gallery(
             photo ?: image ?: background
         }
     },
+    switchImageScale: () -> Unit = {
+        imageScaleType.value = when (imageScaleType.value) {
+            ContentScale.Fit -> ContentScale.Crop
+            ContentScale.Crop -> ContentScale.Fit
+            else -> ContentScale.Fit
+        }
+    },
     handleKey: (event: KeyEvent) -> Boolean = { ev ->
         val action = ev.nativeKeyEvent.action
         if (action == ACTION_DOWN) {
             val code = ev.nativeKeyEvent.keyCode
-            if (infoVisible.value) {
+            if ((code == KEYCODE_ENTER) || (code == KEYCODE_DPAD_CENTER)) {
+                switchImageScale()
+                true
+            } else if (infoVisible.value) {
                 infoVisible.value = true
                 if ((code == KEYCODE_DPAD_DOWN) || (code == KEYCODE_SYSTEM_NAVIGATION_DOWN)) {
                     infoVisible.value = false
@@ -131,17 +145,18 @@ fun Gallery(
         }
     },
     listState: TvLazyListState = rememberTvLazyListState(),
-    imageScaleType: MutableState<ContentScale> = remember { mutableStateOf(ContentScale.Fit) },
+
     customOverlay: @Composable (item: Any?) -> Unit = {}
 ) {
     val initialized = remember { mutableStateOf(false) }
+    val imageState: MutableState<Any?> = remember { mutableStateOf(Color.DarkGray) }
     Box(
         modifier = modifier.background(Color.Black, RectangleShape),
         contentAlignment = Alignment.BottomCenter,
     ) {
         ImageBackground(
             modifier = modifier,
-            image = imageFromItem(currentItemIndex.intValue),
+            imageState = imageState,
         )
         ImmersiveList(
             modifier = Modifier.fillMaxSize(),
@@ -162,11 +177,7 @@ fun Gallery(
                             .pointerInput(Unit) {
                                 detectTapGestures(
                                     onDoubleTap = {
-                                        imageScaleType.value = when (imageScaleType.value) {
-                                            ContentScale.Fit -> ContentScale.Crop
-                                            ContentScale.Crop -> ContentScale.Fit
-                                            else -> ContentScale.Fit
-                                        }
+                                        switchImageScale()
                                     },
                                     onTap = {
                                         infoVisible.toggle()
@@ -178,11 +189,13 @@ fun Gallery(
                                     onSwipeLeft = {
                                         if (currentItemIndex.intValue < (list.size - 1)) {
                                             currentItemIndex.intValue += 1
+                                            imageState.value = list[currentItemIndex.intValue]
                                         }
                                     },
                                     onSwipeRight = {
                                         if (currentItemIndex.intValue > 0) {
                                             currentItemIndex.intValue -= 1
+                                            imageState.value = list[currentItemIndex.intValue]
                                         }
                                     },
                                 )
@@ -205,7 +218,7 @@ fun Gallery(
             ) {
                 ImageBackground(
                     modifier = Modifier.fillMaxWidth(),
-                    image = imageFromItem(currentItemIndex.intValue),
+                    imageState = imageState,
                     transform = { color -> createColorBrush(color, Gravity.BOTTOM) }
                 ) {
                     Spacer(modifier = Modifier.height(32.dp))
@@ -218,7 +231,7 @@ fun Gallery(
                         state = listState
                     ) {
                         itemsIndexed(list) { index, item ->
-                            val fr = FocusRequester()
+                            val fr = rememberFocusRequester()
                             PhotoCard(
                                 modifier = Modifier
                                     .immersiveListItem(index)
@@ -229,6 +242,7 @@ fun Gallery(
                             SideEffect {
                                 val isFocused = (currentItemIndex.intValue == index)
                                 if (isFocused) {
+                                    imageState.value = list[currentItemIndex.intValue]
                                     try {
                                         fr.requestFocus()
                                     } catch (e: Exception) {

+ 3 - 10
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/ItemInfo.kt

@@ -33,10 +33,7 @@ import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.ColorExt.invert
 import org.mjdev.tvlib.helpers.media.MetadataRetriever
 import org.mjdev.tvlib.helpers.media.MetadataRetriever.Companion.rememberMetaDataRetriever
-import org.mjdev.tvlib.interfaces.ItemPhoto
-import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithDate
-import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.ui.components.text.AutoHideEmptyText
 import org.mjdev.tvlib.ui.components.image.ImageBackground
@@ -72,13 +69,9 @@ fun ItemInfo(
             }
         )
     },
+    initialColor: Color = Color.DarkGray,
+    imageState: MutableState<Any?> = mutableStateOf(src),
     metadataRetriever: MetadataRetriever = rememberMetaDataRetriever(),
-    imageFromItem: () -> Any? = {
-        val photo = (src as? ItemPhoto)?.uri
-        val image = (src as? ItemWithImage<*>)?.image
-        val background = (src as? ItemWithBackground<*>)?.background
-        photo ?: image ?: background
-    },
     titleFromItem: () -> Any? = {
         (src as? ItemWithTitle<*>)?.title
     },
@@ -101,7 +94,7 @@ fun ItemInfo(
         ) {
             ImageBackground(
                 modifier = Modifier.fillMaxWidth(),
-                image = imageFromItem(),
+                imageState = imageState,
                 transform = { color -> createColorBrush(color, Gravity.TOP) }
             ) { bckColor ->
                 Column(

+ 21 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/ImageBackground.kt

@@ -12,6 +12,7 @@ import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -19,18 +20,34 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.platform.LocalContext
 import androidx.core.graphics.drawable.toBitmap
 import coil.request.CachePolicy
 import coil.request.ImageRequest
+import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.BitmapExt.majorColor
 import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.conditional
+import org.mjdev.tvlib.interfaces.ItemPhoto
+import org.mjdev.tvlib.interfaces.ItemWithBackground
+import org.mjdev.tvlib.interfaces.ItemWithImage
 
+@TvPreview
 @Composable
 fun ImageBackground(
     modifier: Modifier = Modifier,
-    image: Any? = null,
+    initialColor: Color = Color.DarkGray,
+    imageState: MutableState<Any?> = mutableStateOf(initialColor),
+    imageFromItem: (data:Any?) -> Any? = { data ->
+            val photo = (data as? ItemPhoto)?.uri
+            val img = (data as? ItemWithImage<*>)?.image
+            val background = (data as? ItemWithBackground<*>)?.background
+            val color = data as? Color
+            val string = data.toString()
+            color ?: photo ?: img ?: background ?: string
+    },
+    shape: Shape = RectangleShape,
     transform: ((color: Color) -> Brush)? = null,
     contentAlignment: Alignment = Alignment.TopStart,
     content: @Composable (bckColor: Color) -> Unit = {}
@@ -44,16 +61,16 @@ fun ImageBackground(
                 background(bckColor.value, RectangleShape)
             }
             .conditional(transform != null) {
-                background(transform!!.invoke(bckColor.value), RectangleShape)
+                background(transform!!.invoke(bckColor.value), shape)
             },
         contentAlignment = contentAlignment
     ) {
         content(bckColor.value)
     }
-    LaunchedEffect(image) {
+    LaunchedEffect(imageState.value) {
         bckColor.value = imageLoader.execute(
             ImageRequest.Builder(context)
-                .data(image)
+                .data(imageFromItem(imageState.value))
                 .diskCachePolicy(CachePolicy.DISABLED)
                 .build()
         ).drawable?.toBitmap()?.majorColor(Color.Black) ?: Color.Black

+ 19 - 20
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/DrawerSheet.kt

@@ -20,7 +20,6 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
@@ -32,6 +31,7 @@ import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 
 @Suppress("IllegalExperimentalApiUsage")
 @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalFoundationApi::class)
@@ -51,7 +51,7 @@ fun DrawerSheet(
 ) {
     val initializationComplete = remember { mutableStateOf(false) }
     val focusState = remember { mutableStateOf<FocusState?>(null) }
-    val focusRequester = remember { FocusRequester() }
+    val focusRequester = rememberFocusRequester()
     LaunchedEffect(
         drawerState.currentValue
     ) {
@@ -60,25 +60,24 @@ fun DrawerSheet(
         }
         initializationComplete.value = true
     }
-    val internalModifier = Modifier
-        .focusRequester(focusRequester)
-        .animateContentSize(
-            finishedListener = sizeAnimationFinishedListener
-        )
-        .fillMaxHeight()
-        .then(modifier)
-        .onFocusChanged { state ->
-            focusState.value = state
-            if (initializationComplete.value) {
-                drawerState.setValue(
-                    if (state.hasFocus) DrawerValue.Open
-                    else DrawerValue.Closed
-                )
-            }
-        }
-        .focusGroup()
     Box(
-        modifier = internalModifier
+        modifier = Modifier
+            .focusRequester(focusRequester)
+            .animateContentSize(
+                finishedListener = sizeAnimationFinishedListener
+            )
+            .fillMaxHeight()
+            .then(modifier)
+            .onFocusChanged { state ->
+                focusState.value = state
+                if (initializationComplete.value) {
+                    drawerState.setValue(
+                        if (state.hasFocus) DrawerValue.Open
+                        else DrawerValue.Closed
+                    )
+                }
+            }
+            .focusGroup()
     ) {
         content.invoke(drawerState.currentValue)
     }

+ 32 - 18
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/ModalNavigationDrawer.kt

@@ -10,6 +10,7 @@ package org.mjdev.tvlib.ui.components.navigation
 
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
@@ -21,6 +22,7 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.Dp
@@ -44,35 +46,37 @@ fun ModalNavigationDrawer(
     scrimColor: Color = Color.Black.copy(alpha = 0.5f),
     closeDrawerWidth: Dp = 200.dp,
     contentAlignment: Alignment = Alignment.TopStart,
+    closedDrawerWidth: MutableState<Dp?> = remember { mutableStateOf(null) },
+    onTouchOutside: () -> Unit = {},
     drawerContent: @Composable (DrawerValue) -> Unit = {
         Box(
-            modifier = Modifier
-                .background(Color.LightGray, RectangleShape)
+            modifier = Modifier.background(Color.LightGray, RectangleShape)
         )
     },
     content: @Composable () -> Unit = {
-        Box(modifier = Modifier.fillMaxSize())
-    }
+        Box(
+            modifier = Modifier.fillMaxSize()
+        )
+    },
 ) {
     val localDensity = LocalDensity.current
-    val closedDrawerWidth: MutableState<Dp?> = remember { mutableStateOf(null) }
-    val internalDrawerModifier = Modifier
-        .zIndex(Float.MAX_VALUE)
-        .onSizeChanged {
-            if (closedDrawerWidth.value == null &&
-                drawerState.currentValue == DrawerValue.Closed
-            ) {
-                with(localDensity) {
-                    closedDrawerWidth.value = it.width.toDp()
-                }
-            }
-        }
     Box(
         modifier = modifier,
         contentAlignment = contentAlignment
     ) {
         DrawerSheet(
-            modifier = internalDrawerModifier.align(Alignment.CenterStart),
+            modifier = Modifier
+                .zIndex(Float.MAX_VALUE)
+                .onSizeChanged {
+                    if (closedDrawerWidth.value == null &&
+                        drawerState.currentValue == DrawerValue.Closed
+                    ) {
+                        with(localDensity) {
+                            closedDrawerWidth.value = it.width.toDp()
+                        }
+                    }
+                }
+                .align(Alignment.CenterStart),
             drawerState = drawerState,
             sizeAnimationFinishedListener = { _, targetSize ->
                 if (drawerState.currentValue == DrawerValue.Closed) {
@@ -105,7 +109,17 @@ fun ModalNavigationDrawer(
         ) {
             content()
             if (drawerState.currentValue == DrawerValue.Open) {
-                Canvas(Modifier.fillMaxSize()) {
+                Canvas(
+                    Modifier
+                        .fillMaxSize()
+                        .pointerInput(Unit) {
+                            detectTapGestures(
+                                onTap = {
+                                    onTouchOutside()
+                                }
+                            )
+                        }
+                ) {
                     drawRect(scrimColor)
                 }
             }

+ 115 - 69
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavDrawerContent.kt

@@ -11,16 +11,26 @@ package org.mjdev.tvlib.ui.components.navigation
 import android.annotation.SuppressLint
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableIntState
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvlib.extensions.ComposeExt.isClosed
+import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
+import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.NavControllerExt.open
 import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
@@ -32,8 +42,9 @@ import org.mjdev.tvlib.navigation.NavHostControllerEx
 @Preview
 @Composable
 fun NavDrawerContent(
+    backgroundColor: Color = Color.DarkGray,
+    shape: Shape = RoundedCornerShape(16.dp),
     navController: NavHostControllerEx = rememberNavControllerEx(),
-    backgroundColor: Color = Color(0xff202020),
     onDrawerItemClick: (id: Int) -> Unit = { id ->
         navController.menuItem(id).let { menuItem ->
             if (menuItem.isAction) {
@@ -54,8 +65,11 @@ fun NavDrawerContent(
             }
         }
     },
+    focusedIdx: MutableIntState = remember { mutableIntStateOf(-1) },
+    itemId: (menuItem: MenuItem) -> Int = { menuItem ->
+        navController.indexOfMenuItem(menuItem)
+    },
 ) {
-    val focusedIdx = remember { mutableIntStateOf(-1) }
     val topItems = navController.menuItems.filter { mi ->
         mi.menuGravity == MenuItem.Gravity.Top
     }
@@ -65,82 +79,114 @@ fun NavDrawerContent(
     val centerItems = navController.menuItems.filter { mi ->
         mi.menuGravity == MenuItem.Gravity.Center
     }
-    val itemId: (menuItem: MenuItem) -> Int = { menuItem ->
-        navController.indexOfMenuItem(menuItem)
-    }
-    Column(
-        modifier = Modifier.recomposeHighlighter()
-            .fillMaxHeight()
-            .background(backgroundColor),
-        verticalArrangement = Arrangement.Bottom,
-        horizontalAlignment = Alignment.Start,
-    ) {
-        bottomItems.forEach { menuItem ->
-            if (menuItem.isEnabled) {
-                NavigationRow(
-                    id = itemId(menuItem),
-                    drawerState = navController.menuDrawerState,
-                    icon = menuItem.menuIcon,
-                    text = menuItem.menuText,
-                    onFocus = { id ->
+    val isPortrait = isPortraitMode()
+    val isClosed = navController.menuDrawerState.isClosed
+    Box(
+        modifier = Modifier
+            .recomposeHighlighter()
+            .background(
+                if (isPortrait) {
+                    if (isClosed) Color.Transparent
+                    else backgroundColor
+                } else {
+                    backgroundColor
+                }, shape
+            )
+            .pointerInput(Unit) {
+                detectSwipe(
+                    onSwipeRight = {
                         navController.openMenu()
-                        focusedIdx.value = id
-                        onDrawerItemFocus(id)
                     },
-                    onClick = { id ->
-                        onDrawerItemClick(id)
+                    onSwipeLeft = {
+                        navController.closeMenu()
                     }
                 )
             }
-        }
-    }
-    Column(
-        modifier = Modifier.recomposeHighlighter()
+            .padding(4.dp)
             .fillMaxHeight(),
-        verticalArrangement = Arrangement.Top,
-        horizontalAlignment = Alignment.Start,
     ) {
-        topItems.forEach { menuItem ->
-            if (menuItem.isEnabled) {
-                NavigationRow(
-                    id = itemId(menuItem),
-                    drawerState = navController.menuDrawerState,
-                    icon = menuItem.menuIcon,
-                    text = menuItem.menuText,
-                    onFocus = { id ->
-                        navController.openMenu()
-                        focusedIdx.value = id
-                        onDrawerItemFocus(id)
-                    },
-                    onClick = { id ->
-                        onDrawerItemClick(id)
-                    }
-                )
+        Column(
+            modifier = Modifier
+                .recomposeHighlighter()
+                .fillMaxHeight(),
+            verticalArrangement = Arrangement.Bottom,
+            horizontalAlignment = Alignment.Start,
+        ) {
+            bottomItems.forEach { menuItem ->
+                if (menuItem.isEnabled) {
+                    val id = itemId(menuItem)
+                    NavigationRow(
+                        id = id,
+                        focused = (focusedIdx.value == id),
+                        drawerState = navController.menuDrawerState,
+                        icon = menuItem.menuIcon,
+                        text = menuItem.menuText,
+                        onFocus = { focusId ->
+                            navController.openMenu()
+                            focusedIdx.value = focusId
+                            onDrawerItemFocus(focusId)
+                        },
+                        onClick = { clickId ->
+                            onDrawerItemClick(clickId)
+                        }
+                    )
+                }
             }
         }
-    }
-    Column(
-        modifier = Modifier.recomposeHighlighter()
-            .fillMaxHeight(),
-        verticalArrangement = Arrangement.Center,
-        horizontalAlignment = Alignment.Start,
-    ) {
-        centerItems.forEach { menuItem ->
-            if (menuItem.isEnabled) {
-                NavigationRow(
-                    id = itemId(menuItem),
-                    drawerState = navController.menuDrawerState,
-                    text = menuItem.menuText,
-                    icon = menuItem.menuIcon,
-                    onFocus = { focusId ->
-                        navController.openMenu()
-                        focusedIdx.value = focusId
-                        onDrawerItemFocus(focusId)
-                    },
-                    onClick = { id ->
-                        onDrawerItemClick(id)
-                    }
-                )
+        Column(
+            modifier = Modifier
+                .recomposeHighlighter()
+                .fillMaxHeight(),
+            verticalArrangement = Arrangement.Top,
+            horizontalAlignment = Alignment.Start,
+        ) {
+            topItems.forEach { menuItem ->
+                if (menuItem.isEnabled) {
+                    val id = itemId(menuItem)
+                    NavigationRow(
+                        id = id,
+                        focused = (focusedIdx.value == id),
+                        drawerState = navController.menuDrawerState,
+                        icon = menuItem.menuIcon,
+                        text = menuItem.menuText,
+                        onFocus = { focusId ->
+                            navController.openMenu()
+                            focusedIdx.value = focusId
+                            onDrawerItemFocus(focusId)
+                        },
+                        onClick = { clickId ->
+                            onDrawerItemClick(clickId)
+                        }
+                    )
+                }
+            }
+        }
+        Column(
+            modifier = Modifier
+                .recomposeHighlighter()
+                .fillMaxHeight(),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.Start,
+        ) {
+            centerItems.forEach { menuItem ->
+                if (menuItem.isEnabled) {
+                    val id = itemId(menuItem)
+                    NavigationRow(
+                        id = id,
+                        focused = (focusedIdx.value == id),
+                        drawerState = navController.menuDrawerState,
+                        text = menuItem.menuText,
+                        icon = menuItem.menuIcon,
+                        onFocus = { focusId ->
+                            navController.openMenu()
+                            focusedIdx.value = focusId
+                            onDrawerItemFocus(focusId)
+                        },
+                        onClick = { clickId ->
+                            onDrawerItemClick(clickId)
+                        }
+                    )
+                }
             }
         }
     }

+ 11 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavHostEx.kt

@@ -14,10 +14,12 @@ import androidx.compose.animation.ExitTransition
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.compose.NavHost
 import org.mjdev.tvlib.annotations.TvPreview
@@ -27,6 +29,7 @@ import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvlib.navigation.NavHostControllerEx
+import org.mjdev.tvlib.ui.components.image.FadingPhotoImage
 import org.mjdev.tvlib.ui.components.screen.EmptyScreen
 import org.mjdev.tvlib.ui.components.screen.ScreenView
 
@@ -52,6 +55,14 @@ fun NavHostEx(
         screen(route = EmptyScreen())
     }
 ) {
+    FadingPhotoImage(
+        modifier = Modifier.fillMaxSize(),
+        fadingImageState = navController.backgroundState,
+        contrast = 4f,
+        alpha = 0.7f,
+        brightness = -255f,
+        contentScale = ContentScale.Crop
+    )
     ScreenView(
         modifier = modifier.recomposeHighlighter(),
         navController = navController,

+ 11 - 25
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/Navigation.kt

@@ -9,20 +9,13 @@
 package org.mjdev.tvlib.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.layout.fillMaxSize
 import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.statusBarsPadding
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.annotations.TvPreview
@@ -41,14 +34,7 @@ import org.mjdev.tvlib.ui.components.page.Page
 fun Navigation(
     modifier: Modifier = Modifier,
     navController: NavHostControllerEx = rememberNavControllerEx(),
-    backgroundColor: Color = Color(0xff202020),
-    roundCornerSize: Dp = 8.dp,
-    borderSize: Dp = 0.dp,
-    borderColor: Color = Color.Transparent,
-    shape: Shape = RoundedCornerShape(roundCornerSize),
-    content: @Composable () -> Unit = {
-        Page()
-    },
+    content: @Composable () -> Unit = { Page() },
     menuItems: List<MenuItem> = if (isEditMode()) listOf(
         MenuItem.MENU_ITEM_EXIT,
         MenuItem.MENU_ITEM_SETTINGS,
@@ -74,7 +60,11 @@ fun Navigation(
             .recomposeHighlighter()
     ) {
         SettingsDrawer(
-            modifier = Modifier.fillMaxSize()
+            modifier = Modifier.fillMaxSize(),
+            drawerState = navController.settingsDrawerState,
+            onTouchOutside = {
+                navController.closeSettings()
+            }
         ) {
             if (navController.isMenuEnabled) {
                 val drawerContent: @Composable (DrawerValue) -> Unit = { state ->
@@ -84,7 +74,6 @@ fun Navigation(
                             .statusBarsPadding()
                     ) {
                         NavDrawerContent(
-                            backgroundColor = backgroundColor,
                             navController = navController,
                         )
                     }
@@ -92,20 +81,17 @@ fun Navigation(
                 }
                 if (modal) {
                     ModalNavigationDrawer(
-                        modifier = modifier
-                            .fillMaxHeight()
-                            .background(backgroundColor, shape)
-                            .border(borderSize, borderColor, shape),
+                        modifier = modifier.fillMaxHeight(),
                         drawerState = navController.menuDrawerState,
                         content = mainContent,
                         drawerContent = drawerContent,
+                        onTouchOutside = {
+                            navController.closeMenu()
+                        }
                     )
                 } else {
                     NavigationDrawer(
-                        modifier = modifier
-                            .fillMaxHeight()
-                            .background(backgroundColor, shape)
-                            .border(borderSize, borderColor, shape),
+                        modifier = modifier.fillMaxHeight(),
                         drawerState = navController.menuDrawerState,
                         content = mainContent,
                         drawerContent = drawerContent

+ 0 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationDrawer.kt

@@ -33,7 +33,6 @@ fun NavigationDrawer(
     drawerState: DrawerState = rememberDrawerState(
         if (isEditMode()) DrawerValue.Open else DrawerValue.Closed
     ),
-//    contentAlignment: Alignment = Alignment.TopStart,
     drawerContent: @Composable (DrawerValue) -> Unit = {
         Box(
             modifier = Modifier

+ 33 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavigationRow.kt

@@ -9,6 +9,10 @@
 package org.mjdev.tvlib.ui.components.navigation
 
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
@@ -20,9 +24,11 @@ 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.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
@@ -38,12 +44,16 @@ import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ComposeExt.isFocused
 import org.mjdev.tvlib.extensions.ComposeExt.isLandscapeMode
 import org.mjdev.tvlib.extensions.ComposeExt.isOpen
+import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
+import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ModifierExt.focusState
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
+import org.mjdev.tvlib.ui.components.card.FocusHelper
 import org.mjdev.tvlib.ui.components.complex.FocusableBox
 import org.mjdev.tvlib.ui.components.icon.IconAny
 import org.mjdev.tvlib.ui.components.text.TextAny
+import timber.log.Timber
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @TvPreview
@@ -64,12 +74,22 @@ fun NavigationRow(
     margin: Dp = 0.dp,
     padding: Dp = 4.dp,
     drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
-    focusState: MutableState<FocusState?> = rememberFocusState(text),
+    focused: Boolean = isEditMode(),
+    focusState: MutableState<FocusState?> = rememberFocusState(
+        text,
+        FocusHelper(focused)
+    ),
+    focusRequester: FocusRequester = rememberFocusRequester(),
     onFocus: (id: Int) -> Unit = {},
     onClick: (id: Int) -> Unit = {},
 ) {
     val isEdit = isEditMode()
     val isShown = isLandscapeMode() || drawerState.isOpen
+    val isPortrait = isPortraitMode()
+    val enter = if (isPortrait) expandVertically()
+    else (fadeIn() + expandVertically())
+    val exit = if (isPortrait) shrinkVertically()
+    else (fadeOut() + shrinkVertically())
     if (isShown) {
         FocusableBox(
             modifier = modifier
@@ -86,6 +106,7 @@ fun NavigationRow(
                 .focusState(focusState),
             shape = shape,
             focusedColor = focusedColor.copy(alpha = 0.5f),
+            focusRequester = focusRequester,
             unFocusedColor = unFocusedColor,
             onFocusChange = { state ->
                 if (state.isFocused || state.hasFocus) {
@@ -115,6 +136,8 @@ fun NavigationRow(
                     )
                 }
                 AnimatedVisibility(
+                    enter = enter,
+                    exit = exit,
                     visible = (isEdit || (drawerState.currentValue == DrawerValue.Open))
                 ) {
                     TextAny(
@@ -130,5 +153,14 @@ fun NavigationRow(
                 }
             }
         }
+        LaunchedEffect(focused) {
+            try {
+                if (focused) {
+                    focusRequester.requestFocus()
+                }
+            } catch (e: Exception) {
+                Timber.e(e)
+            }
+        }
     }
 }

+ 2 - 15
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt

@@ -10,14 +10,9 @@ package org.mjdev.tvlib.ui.components.navigation
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
 import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
@@ -30,14 +25,10 @@ import org.mjdev.tvlib.extensions.ComposeExt
 @Composable
 fun SettingsDrawer(
     modifier: Modifier = Modifier,
-    backgroundColor: Color = Color(0xff202020),
-    roundCornerSize: Dp = 16.dp,
-    borderSize: Dp = 0.dp,
-    borderColor: Color = Color.Transparent,
-    shape: Shape = RoundedCornerShape(roundCornerSize),
     drawerState: DrawerState = rememberDrawerState(
         if (ComposeExt.isEditMode()) DrawerValue.Open else DrawerValue.Closed
     ),
+    onTouchOutside: () -> Unit = {},
     content: @Composable () -> Unit = {
         Box(modifier = Modifier.fillMaxSize())
     },
@@ -46,13 +37,9 @@ fun SettingsDrawer(
         modifier = modifier,
         contentAlignment = Alignment.TopEnd,
         drawerState = drawerState,
+        onTouchOutside = onTouchOutside,
         drawerContent = {
             SettingsDrawerContent(
-                backgroundColor = backgroundColor,
-                roundCornerSize = roundCornerSize,
-                borderSize = borderSize,
-                borderColor = borderColor,
-                shape = shape
             )
         }
     ) {

+ 1 - 16
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawerContent.kt

@@ -9,20 +9,14 @@
 package org.mjdev.tvlib.ui.components.navigation
 
 import android.annotation.SuppressLint
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.conditional
@@ -32,13 +26,6 @@ import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 @Preview
 @Composable
 fun SettingsDrawerContent(
-    backgroundColor: Color = Color(0xff202020),
-    roundCornerSize: Dp = 16.dp,
-    borderSize: Dp = 0.dp,
-    borderColor: Color = Color.Transparent,
-    shape: Shape = RoundedCornerShape(roundCornerSize),
-//    onDrawerItemClick: (id: Int) -> Unit = { id -> },
-//    onDrawerItemFocus: (id: Int) -> Unit = { id -> },
 ) {
     Column(
         modifier = Modifier
@@ -46,9 +33,7 @@ fun SettingsDrawerContent(
             .fillMaxHeight()
             .conditional(isEditMode()) {
                 width(200.dp)
-            }
-            .background(backgroundColor, shape)
-            .border(borderSize, borderColor),
+            },
         verticalArrangement = Arrangement.Top,
         horizontalAlignment = Alignment.Start,
     ) {

+ 13 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/Page.kt

@@ -43,16 +43,22 @@ open class Page {
     fun content() {
         val focusRequester = rememberFocusRequester(title)
         Column(
-            Modifier.fillMaxSize().recomposeHighlighter()
+            Modifier
+                .fillMaxSize()
+                .recomposeHighlighter()
         ) {
             VerticalScrollableBox(
-                modifier = Modifier.recomposeHighlighter()
+                modifier = Modifier
+                    .recomposeHighlighter()
                     .fillMaxSize()
-                    .requestFocusOnTouch(focusRequester),
+                    .requestFocusOnTouch(
+                        focusRequester = focusRequester
+                    ),
                 contentAlignment = Alignment.Center,
             ) {
                 Box(
-                    modifier = Modifier.recomposeHighlighter()
+                    modifier = Modifier
+                        .recomposeHighlighter()
                         .fillMaxSize()
                         .focusRequester(focusRequester)
                 ) {
@@ -65,7 +71,9 @@ open class Page {
     @Composable
     open fun Content() {
         Column(
-            modifier = Modifier.fillMaxSize().recomposeHighlighter(),
+            modifier = Modifier
+                .fillMaxSize()
+                .recomposeHighlighter(),
             verticalArrangement = Arrangement.Center,
             horizontalAlignment = Alignment.CenterHorizontally,
         ) {

+ 9 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/PagerScope.kt

@@ -16,11 +16,12 @@ import org.mjdev.tvlib.extensions.ListExt.addUnique
 import org.mjdev.tvlib.navigation.MenuItem
 import org.mjdev.tvlib.navigation.NavHostControllerEx
 
-@Suppress("unused", "PropertyName")
+@Suppress("unused", "PropertyName", "UNUSED_ANONYMOUS_PARAMETER")
 class PagerScope(
     val navController: NavHostControllerEx,
-    startIndex:Int = 0,
+    startIndex: Int = 0,
     pages: PagerScope.() -> Unit = {},
+    val onPageChange: (index: Int, page: Page?) -> Unit = { index, page -> },
 ) : ArrayList<Page>() {
 
     val Bottom = MenuItem.Gravity.Bottom
@@ -71,20 +72,24 @@ class PagerScope(
     fun scrollToPage(index: Int) {
         if (size > index && size > -1) {
             currentPage.value = index
+            onPageChange(index, this[index])
         }
     }
 
 }
 
+@Suppress("UNUSED_ANONYMOUS_PARAMETER")
 @Composable
 fun rememberPagerScope(
     navController: NavHostControllerEx,
-    startIndex:Int = 0,
-    pages: PagerScope.() -> Unit = {}
+    startIndex: Int = 0,
+    pages: PagerScope.() -> Unit = {},
+    onPageChange: (index: Int, page: Page?) -> Unit = { index, page -> },
 ) = remember {
     PagerScope(
         navController,
         startIndex,
         pages,
+        onPageChange,
     )
 }

+ 3 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/TvPager.kt

@@ -47,6 +47,8 @@ fun TvPager(
     roundCornerSize: Dp = 0.dp,
     backGroundColor: Color = Color.Transparent,
     backGroundShape: Shape = RoundedCornerShape(roundCornerSize),
+    @Suppress("UNUSED_ANONYMOUS_PARAMETER")
+    onPageChange: (index: Int, page: Page?) -> Unit = { index, page -> },
     pages: PagerScope.() -> Unit = {
         page(EMPTY_PAGE)
     }
@@ -54,7 +56,7 @@ fun TvPager(
 
     val currentPage = remember { mutableStateOf<Page?>(null) }
     val coroutineScope = rememberCoroutineScope()
-    val pagerScope = rememberPagerScope(navController, startIndex, pages)
+    val pagerScope = rememberPagerScope(navController, startIndex, pages, onPageChange)
 
     val goToPage: (
         page: Page?

+ 1 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/screen/ScreenView.kt

@@ -26,9 +26,7 @@ import org.mjdev.tvlib.ui.components.page.Page
 fun ScreenView(
     modifier: Modifier = Modifier,
     navController: NavHostControllerEx = rememberNavControllerEx(),
-    content: @Composable () -> Unit = {
-        Page()
-    }
+    content: @Composable () -> Unit = { Page() }
 ) {
     Box(
         modifier = modifier

+ 65 - 95
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/ErrorMessage.kt

@@ -9,16 +9,19 @@
 package org.mjdev.tvlib.ui.components.tv
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.takeOrElse
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
@@ -27,7 +30,6 @@ 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.constraintlayout.compose.ConstraintLayout
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.LocalContentColor
 import androidx.tv.material3.LocalTextStyle
@@ -37,7 +39,6 @@ import org.mjdev.tvlib.ui.components.button.Button
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.ui.components.text.TextAny
 
-@Suppress("LocalVariableName")
 @OptIn(ExperimentalTvMaterial3Api::class)
 @TvPreview
 @Composable
@@ -54,106 +55,75 @@ fun ErrorMessage(
     backgroundColor: Color = Color.Red,
     roundSize: Dp = 8.dp,
     cancelText: Any? = stringResource(id = R.string.bt_dismiss),
+    shape: Shape = RoundedCornerShape(roundSize),
     dismissible: Boolean = true,
     onDismiss: () -> Unit = {}
+) = Row(
+    modifier = Modifier
+        .fillMaxWidth()
+        .background(backgroundColor, shape)
+        .recomposeHighlighter(),
+    verticalAlignment = Alignment.CenterVertically,
 ) {
-
-    Box(
+    val style = LocalTextStyle.current
+    val textColor = color.takeOrElse {
+        style.color.takeOrElse {
+            LocalContentColor.current
+        }
+    }
+    TextAny(
         modifier = Modifier
-            .fillMaxWidth()
             .recomposeHighlighter()
-    ) {
-
-        ConstraintLayout(
+            .wrapContentSize()
+            .padding(paddingTitle),
+        text = title ?: stringResource(id = R.string.title_error),
+        fontWeight = fontWeight,
+        fontSize = fontSizeTitle,
+        color = color
+    )
+    Spacer(
+        modifier = Modifier.weight(1f)
+    )
+    BasicText(
+        modifier = Modifier
+            .recomposeHighlighter()
+            .wrapContentSize()
+            .padding(paddingMessage),
+        style = style.merge(
+            color = textColor,
+            fontSize = fontSizeMessage,
+            fontWeight = fontWeight,
+            textAlign = TextAlign.Left,
+        ),
+        minLines = 1,
+        maxLines = 4,
+        text =
+        error?.message ?: (stringResource(id = R.string.error_unknown) +
+                "\nType: " +
+                (error?.javaClass?.simpleName ?: Exception::class.simpleName))
+    )
+    if (dismissible && (cancelText != null)) {
+        Spacer(
+            modifier = Modifier.weight(1f)
+        )
+        Button(
             modifier = Modifier
                 .recomposeHighlighter()
-                .fillMaxWidth()
-                .padding(contentPadding)
-                .background(backgroundColor, RoundedCornerShape(roundSize))
-        ) {
-
-            val style = LocalTextStyle.current
-
-            val (_title, _message, _button) = createRefs()
-
-            val textColor = color.takeOrElse {
-                style.color.takeOrElse {
-                    LocalContentColor.current
-                }
-            }
-
-            TextAny(
-                modifier = Modifier
-                    .recomposeHighlighter()
-                    .wrapContentSize()
-                    .constrainAs(_title) {
-                        top.linkTo(parent.top)
-                        start.linkTo(parent.start, margin = roundSize)
-                        bottom.linkTo(parent.bottom)
-                    }
-                    .padding(paddingTitle),
-                text = title ?: stringResource(id = R.string.title_error),
-                fontWeight = fontWeight,
-                fontSize = fontSizeTitle,
-                color = color
-            )
-
-            BasicText(
-                modifier = Modifier
-                    .recomposeHighlighter()
-                    .wrapContentSize()
-                    .constrainAs(_message) {
-                        top.linkTo(parent.top)
-                        start.linkTo(_title.end)
-                        end.linkTo(_button.start)
-                        bottom.linkTo(parent.bottom)
-                    }
-                    .padding(paddingMessage),
-                style = style.merge(
-                    color = textColor,
-                    fontSize = fontSizeMessage,
-                    fontWeight = fontWeight,
-                    textAlign = TextAlign.Left,
-                ),
-                minLines = 1,
-                maxLines = 4,
-                text =
-                error?.message ?: (stringResource(id = R.string.error_unknown) +
-                        "\nType: " +
-                        (error?.javaClass?.simpleName ?: Exception::class.simpleName))
-            )
-
-            if (dismissible && (cancelText != null)) {
-
-                Button(
-                    modifier = Modifier
-                        .recomposeHighlighter()
-                        .padding(4.dp)
-                        .constrainAs(_button) {
-                            top.linkTo(parent.top, margin = 4.dp)
-                            end.linkTo(parent.end, margin = roundSize)
-                            bottom.linkTo(parent.bottom, margin = 4.dp)
-                        },
-                    contentPadding = PaddingValues(
-                        8.dp,
-                        4.dp
-                    ),
-                    containerColor = Color.Red,
-                    onClick = {
-                        onDismiss()
-                    },
-                    content = {
-                        TextAny(
-                            text = cancelText,
-                            color = color
-                        )
-                    }
+                .padding(4.dp),
+            contentPadding = PaddingValues(
+                8.dp,
+                4.dp
+            ),
+            containerColor = Color.Red,
+            onClick = {
+                onDismiss()
+            },
+            content = {
+                TextAny(
+                    text = cancelText,
+                    color = color
                 )
-
             }
-
-        }
-
+        )
     }
-
 }

+ 18 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Header.kt

@@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -32,8 +34,9 @@ import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.R
-import org.mjdev.tvlib.extensions.ComposeExt.isLandscapeMode
+import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
 import org.mjdev.tvlib.ui.components.badge.Badge
+import org.mjdev.tvlib.ui.components.complex.FocusableBox
 
 @TvPreview
 @Composable
@@ -55,7 +58,7 @@ fun Header(
     onUserPicClick: () -> Unit = {},
 ) {
     val isEdit = isEditMode()
-    val isClockShown = isLandscapeMode()
+    val isPortrait = isPortraitMode()
     Box(
         modifier = Modifier
             .recomposeHighlighter()
@@ -71,7 +74,7 @@ fun Header(
             horizontalArrangement = Arrangement.End,
             verticalAlignment = Alignment.CenterVertically,
         ) {
-            if (isClockShown) {
+            if (!isPortrait) {
                 Clock(
                     modifier = Modifier
                         .wrapContentSize()
@@ -117,6 +120,18 @@ fun Header(
             horizontalArrangement = Arrangement.Start,
             verticalAlignment = Alignment.CenterVertically,
         ) {
+            FocusableBox(
+                modifier = Modifier
+                    .recomposeHighlighter()
+                    .wrapContentHeight()
+                    .width(2.dp)
+                    .clip(CircleShape),
+                onFocusChange = { state ->
+                    if ((!isPortrait) && state.isFocused) {
+                        onTitleClick()
+                    }
+                }
+            )
             Title(
                 modifier = Modifier
                     .wrapContentSize()

+ 3 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Title.kt

@@ -57,7 +57,9 @@ fun Title(
         onClick = onClick,
         shape = RoundedCornerShape(8.dp),
         focusedColor = Color.Transparent,
-        onFocusChange = { state -> focusState.value = state }
+        onFocusChange = { state ->
+            focusState.value = state
+        }
     ) {
         Row(
             modifier = modifier