فهرست منبع

touch handling

Milan Jurkulak 3 سال پیش
والد
کامیت
90f8b87eff
18فایلهای تغییر یافته به همراه294 افزوده شده و 237 حذف شده
  1. 48 19
      app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ModifierExt.kt
  2. 5 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/button/Button.kt
  3. 25 18
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/CarouselCard.kt
  4. 34 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/ItemCard.kt
  5. 51 54
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/card/PhotoCard.kt
  6. 5 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Badge.kt
  7. 1 21
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/BigCarousel.kt
  8. 6 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Clock.kt
  9. 22 30
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableBox.kt
  10. 17 15
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/FocusableCard.kt
  11. 5 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Navigation.kt
  12. 7 1
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/NavigationRow.kt
  13. 3 2
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Tabs.kt
  14. 49 51
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Title.kt
  15. 6 0
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/UserPic.kt
  16. 2 18
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/image/ImageAny.kt
  17. 3 3
      app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/page/Page.kt
  18. 5 4
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt

+ 48 - 19
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ModifierExt.kt

@@ -8,40 +8,69 @@
 
 package org.mjdev.tvapp.base.extensions
 
-import androidx.compose.foundation.focusable
+import android.annotation.SuppressLint
 import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.Composable
+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
+import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.input.pointer.pointerInput
 import timber.log.Timber
 
 object ModifierExt {
 
+    @Composable
+    fun rememberMutableInteractionSource() = remember { MutableInteractionSource() }
+
+    @Composable
+    fun rememberFocusState(
+        initial: FocusState? = null
+    ) = remember {
+        mutableStateOf(initial)
+    }
+
+    val MutableState<FocusState?>.isFocused
+        get() = (value?.isFocused == true) || (value?.hasFocus == true)
+
+    @SuppressLint("ComposableModifierFactory")
+    @Composable
     fun Modifier.touchable(
-        focusable: Boolean = false,
-        onTouch: FocusRequester.() -> Unit = {},
-    ): Modifier = composed {
-        val focusRequester = remember { FocusRequester() }
-        this
-            .apply {
-                if (focusable) {
-                    this.focusable()
+        state: MutableState<FocusState?> = rememberFocusState(),
+        focusRequester: FocusRequester = remember { FocusRequester() },
+        onFocus:()->Unit={},
+        onClick:()->Unit={},
+    ): Modifier = focusRequester(
+        focusRequester
+    ).pointerInput(Unit) {
+        detectTapGestures {
+            if(state.isFocused) {
+                onClick()
+            } else {
+                try {
+                    focusRequester.requestFocus()
+                    onFocus()
+                } catch (e: Exception) {
+                    Timber.e(e)
                 }
             }
-            .focusRequester(focusRequester)
-            .pointerInput(Unit) {
-                detectTapGestures {
-                    try {
-                        onTouch(focusRequester)
-                        focusRequester.requestFocus()
-                    } catch (e: Exception) {
-                        Timber.e(e)
-                    }
-                }
+        }
+    }.onFocusChanged { fstate ->
+        state.value = fstate
+    }.apply {
+        if (state.isFocused) {
+            try {
+                focusRequester.requestFocus()
+            } catch (e: Exception) {
+                Timber.e(e)
             }
+        }
     }
 
 }

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

@@ -12,7 +12,9 @@ import android.annotation.SuppressLint
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.runtime.Composable
+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.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
@@ -27,6 +29,7 @@ import androidx.tv.material3.ButtonGlow
 import androidx.tv.material3.ButtonScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.Glow
+import org.mjdev.tvapp.base.extensions.ModifierExt.rememberFocusState
 import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
@@ -47,13 +50,14 @@ fun Button(
     shape: Shape = RectangleShape,
     glow: ButtonGlow = ButtonDefaults.NO_GLOW,
     scale: ButtonScale = ButtonDefaults.NO_SCALE,
+    focusState: MutableState<FocusState?> = rememberFocusState(),
     content: @Composable RowScope.() -> Unit = {
         TextAny(text = text)
     }
 ) {
 
     Button(
-        modifier = modifier.touchable(),
+        modifier = modifier.touchable(focusState),
         glow = glow,
         scale = scale,
         onClick = onClick,

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

@@ -11,7 +11,9 @@ package org.mjdev.tvapp.base.ui.components.card
 import android.annotation.SuppressLint
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+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
@@ -20,8 +22,10 @@ import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.CompactCard
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.Glow
 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.extensions.ModifierExt.touchable
 import org.mjdev.tvapp.base.interfaces.ItemWithBackground
 import org.mjdev.tvapp.base.interfaces.ItemWithDescription
 import org.mjdev.tvapp.base.interfaces.ItemWithSubtitle
@@ -39,20 +43,31 @@ fun CarouselCard(
     modifier: Modifier = Modifier,
     contentScale: ContentScale = ContentScale.Crop,
     scale: CardScale = CardScale.None,
+    placeholder: @Composable () -> Unit = {},
+    focusState: MutableState<FocusState?> = rememberFocusState(),
+    imageRenderer: @Composable (modifier: Modifier) -> Unit = {
+        ImageAny(
+            modifier = modifier,
+            src = (item as? ItemWithBackground)?.backgroundImageUrl,
+            contentDescription = (item as? ItemWithDescription)?.description?.toString(),
+            contentScale = contentScale,
+            placeholder = placeholder
+        )
+    },
+    onFocus: () -> Unit = {},
     onClick: () -> Unit = {}
 ) {
 
     CompactCard(
-        modifier = modifier,
+        modifier = modifier.touchable(
+            state = focusState,
+            onClick = onClick,
+            onFocus = onFocus,
+        ),
+        border = CardDefaults.colorFocusBorder(Color.Green),
         scale = scale,
         image = {
-            ImageAny(
-                modifier = modifier,
-                src = (item as? ItemWithBackground)?.backgroundImageUrl,
-                contentDescription = (item as? ItemWithDescription)?.description?.toString(),
-                contentScale = contentScale,
-                placeholder = placeHolder,
-            )
+            imageRenderer(modifier)
         },
         title = {
             TextAny(
@@ -67,16 +82,8 @@ fun CarouselCard(
             )
         },
         description = {
-          // todo
+            // todo
         },
-        glow = CardDefaults.glow(
-            glow = Glow.None,
-            focusedGlow = Glow(
-                elevationColor = Color.Green,
-                elevation = 10.dp
-            ),
-            pressedGlow = Glow.None
-        ),
         onClick = onClick
     )
 

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

@@ -9,14 +9,27 @@
 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.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
+import androidx.tv.material3.CardBorder
+import androidx.tv.material3.CardColors
+import androidx.tv.material3.CardDefaults
+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.interfaces.ItemWithDescription
+import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.ui.components.complex.FocusableCard
+import org.mjdev.tvapp.base.ui.components.image.ImageAny
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Preview
@@ -25,6 +38,23 @@ import org.mjdev.tvapp.base.ui.components.complex.FocusableCard
 fun ItemCard(
     item: Any? = null,
     modifier: Modifier = Modifier,
+    contentScale: ContentScale = ContentScale.Crop,
+    scale: CardScale = CardDefaults.scale(),
+    shape: CardShape = CardDefaults.shape(),
+    colors: CardColors = CardDefaults.colors(),
+    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,
+            src = (item as? ItemWithImage)?.imageUrl,
+            contentDescription = (item as? ItemWithDescription)?.description?.toString(),
+            contentScale = contentScale,
+            placeholder = placeholder
+        )
+    },
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
 ) {
@@ -34,6 +64,10 @@ fun ItemCard(
             .widthIn(max = 320.dp)
             .aspectRatio(16f / 9f),
         item = item,
+        contentScale = contentScale,
+        scale = scale,
+        imageRenderer = imageRenderer,
+        placeholder = placeholder,
         onFocus = onFocus,
         onClick = onClick,
     )

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

@@ -1,54 +1,51 @@
-///*
-// * 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.annotation.SuppressLint
-//import androidx.compose.runtime.Composable
-//import androidx.compose.ui.Modifier
-//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.interfaces.ItemWithDescription
-//import org.mjdev.tvapp.base.interfaces.ItemWithImage
-//import org.mjdev.tvapp.base.ui.components.complex.FocusableCard
-//import org.mjdev.tvapp.base.ui.components.image.PhotoImage
-//
-//@OptIn(ExperimentalTvMaterial3Api::class)
-//@SuppressLint("ModifierParameter")
-//@Preview
-//@Composable
-//fun PhotoCard(
-//    item: Any? = null,
-//    modifier: Modifier = Modifier,
-//    contentScale: ContentScale = ContentScale.Crop,
-//    imageRenderer: @Composable (modifier: Modifier) -> Unit = { m ->
-//        PhotoImage(
-//            modifier = m,
-//            src = (item as? ItemWithImage)?.imageUrl,
-//            contentDescription = (item as? ItemWithDescription)?.description?.toString(),
-//        )
-//    },
-//    placeholder: @Composable () -> Unit = {},
-//    scale: CardScale = CardDefaults.scale(),
-//    onFocus: (item: Any?) -> Unit = {},
-//    onClick: (item: Any?) -> Unit = {},
-//) {
-//    ItemCard(
-//        item = item,
-//        modifier = modifier,
-//        contentScale = contentScale,
-//        scale = scale,
-//        imageRenderer = imageRenderer,
-//        placeholder = placeholder,
-//        onClick = onClick,
-//        onFocus = onFocus
-//    )
-//}
+/*
+ * 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.annotation.SuppressLint
+import androidx.compose.ui.Modifier
+import androidx.compose.runtime.Composable
+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.interfaces.ItemWithDescription
+import org.mjdev.tvapp.base.interfaces.ItemWithImage
+import org.mjdev.tvapp.base.ui.components.image.PhotoImage
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@SuppressLint("ModifierParameter")
+@Preview
+@Composable
+fun PhotoCard(
+    item: Any? = null,
+    modifier: Modifier = Modifier,
+    contentScale: ContentScale = ContentScale.Crop,
+    imageRenderer: @Composable (modifier: Modifier) -> Unit = { m ->
+        PhotoImage(
+            modifier = m,
+            src = (item as? ItemWithImage)?.imageUrl,
+            contentDescription = (item as? ItemWithDescription)?.description?.toString(),
+        )
+    },
+    placeholder: @Composable () -> Unit = {},
+    scale: CardScale = CardDefaults.scale(),
+    onFocus: (item: Any?) -> Unit = {},
+    onClick: (item: Any?) -> Unit = {},
+) = ItemCard(
+    item = item,
+    modifier = modifier,
+    contentScale = contentScale,
+    scale = scale,
+    imageRenderer = imageRenderer,
+    placeholder = placeholder,
+    onClick = onClick,
+    onFocus = onFocus
+)

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

@@ -10,8 +10,10 @@ package org.mjdev.tvapp.base.ui.components.complex
 
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.tooling.preview.Preview
@@ -21,6 +23,7 @@ import androidx.compose.ui.unit.dp
 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.text.CircleText
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -36,12 +39,14 @@ fun Badge(
     textColor: Color = Color.White,
     textSize: TextUnit = 20.sp,
     contentPadding: Dp = 2.dp,
+    focusState: MutableState<FocusState?> = rememberFocusState(),
     onFocus: () -> Unit = {},
     onClick: () -> Unit = {}
 ) {
 
     FocusableBox(
         modifier = modifier.clip(CircleShape),
+        focusState = focusState,
         shape = shape,
         onFocus = onFocus,
         onClick = onClick

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

@@ -12,14 +12,9 @@ import android.annotation.SuppressLint
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableIntStateOf
-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.focusRequester
-import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
@@ -27,7 +22,6 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.Carousel
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import org.mjdev.tvapp.base.extensions.ModifierExt.touchable
 import org.mjdev.tvapp.base.ui.components.card.CarouselCard
 import org.mjdev.tvapp.data.Movie
 
@@ -44,34 +38,20 @@ fun BigCarousel(
     onItemClicked: (movie: Movie?) -> Unit = {},
 ) {
 
-    val focusRequester = remember { FocusRequester() }
     val itemIndex = remember { mutableIntStateOf(0) }
     val selectedMovie: () -> Movie? = { items[itemIndex.value] }
-    val isFocused: MutableState<Boolean> = remember { mutableStateOf(false) }
 
     Carousel(
         itemCount = items.size,
         modifier = modifier
-            .focusRequester(focusRequester)
             .fillMaxWidth()
-            .height(height)
-            .onFocusChanged { focusState ->
-                isFocused.value = focusState.isFocused || focusState.hasFocus
-            },
+            .height(height),
     ) { indexOfCarouselItem ->
 
         itemIndex.value = indexOfCarouselItem
 
         CarouselCard(
             modifier = modifier
-                .touchable {
-                    if (isFocused.value)
-                        onItemClicked(selectedMovie())
-                    else {
-                        onItemSelected(selectedMovie())
-                        focusRequester.requestFocus()
-                    }
-                }
                 .fillMaxWidth()
                 .height(height),
             contentScale = ContentScale.Crop,

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

@@ -15,10 +15,12 @@ import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.collectAsState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.font.FontWeight
@@ -33,6 +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.text.TextAny
 
 @Preview
@@ -51,6 +55,7 @@ fun Clock(
     contentPadding: Dp = 2.dp,
     showTime: Boolean = true,
     showDate: Boolean = true,
+    focusState: MutableState<FocusState?> = rememberFocusState(),
     onClick: () -> Unit = {},
 ) {
 
@@ -88,6 +93,7 @@ fun Clock(
 
     FocusableBox(
         modifier = modifier.padding(contentPadding),
+        focusState = focusState,
         shape = RoundedCornerShape(roundSize),
         onClick = onClick
     ) {

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

@@ -14,10 +14,8 @@ import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusEvent
+import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.tooling.preview.Preview
@@ -27,6 +25,8 @@ import androidx.tv.material3.Border
 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)
@@ -38,7 +38,7 @@ fun FocusableBox(
     onFocus: () -> Unit = {},
     enabled: Boolean = true,
     tonalElevation: Dp = 0.dp,
-    focusedColor: Color = Color.Black,
+    focusedColor: Color = Color.Green,
     unFocusedColor: Color = Color.Transparent,
     pressedColor: Color = focusedColor.copy(alpha = 0.5f),
     disabledColor: Color = Color.Gray.copy(alpha = 0.5f),
@@ -46,31 +46,23 @@ fun FocusableBox(
     shape: Shape = RoundedCornerShape(roundCornerSize),
     borderColor: Color = Color.Transparent,
     borderSize: Dp = 0.dp,
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    isFocused: MutableState<Boolean> = remember { mutableStateOf(false) },
-    onTouched: (focused: Boolean) -> Unit = { focused ->
-        if (focused) onClick() else onFocus()
-    },
+    interactionSource: MutableInteractionSource = rememberMutableInteractionSource(),
+    focusState: MutableState<FocusState?> = rememberFocusState(),
     content: @Composable BoxScope.() -> Unit = {}
 ) {
 
     Surface(
-        onClick,
-        modifier
-            .touchable {
-                onTouched(isFocused.value)
-            }
-            .onFocusEvent { focusState ->
-                isFocused.value = (focusState.isFocused || focusState.hasFocus)
-                if (isFocused.value) {
-                    onFocus()
-                }
-            },
-        null,
-        enabled,
-        tonalElevation,
-        ClickableSurfaceDefaults.shape(shape),
-        ClickableSurfaceDefaults.colors(
+        onClick = onClick,
+        modifier = modifier.touchable(
+            state = focusState,
+            onFocus = onFocus,
+            onClick = onClick,
+        ),
+        onLongClick = null,
+        enabled = enabled,
+        tonalElevation = tonalElevation,
+        shape = ClickableSurfaceDefaults.shape(shape),
+        colors = ClickableSurfaceDefaults.colors(
             containerColor = unFocusedColor,
             contentColor = unFocusedColor,
             focusedContainerColor = focusedColor,
@@ -80,14 +72,14 @@ fun FocusableBox(
             disabledContainerColor = disabledColor,
             disabledContentColor = disabledColor
         ),
-        ClickableSurfaceDefaults.scale(
+        scale = ClickableSurfaceDefaults.scale(
             1f,
             1f,
             1f,
             1f,
             1f
         ),
-        Border(
+        border = Border(
             border = BorderStroke(
                 width = borderSize,
                 color = borderColor
@@ -95,9 +87,9 @@ fun FocusableBox(
             inset = 0.dp,
             shape = shape
         ).let { b -> ClickableSurfaceDefaults.border(b, b, b, b, b) },
-        ClickableSurfaceDefaults.glow(),
-        interactionSource,
-        content
+        glow = ClickableSurfaceDefaults.glow(),
+        interactionSource = interactionSource,
+        content = content
     )
 
 }

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

@@ -12,8 +12,10 @@ 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
@@ -29,12 +31,14 @@ import androidx.tv.material3.CardScale
 import androidx.tv.material3.CardShape
 import androidx.tv.material3.CompactCard
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import org.mjdev.tvapp.R
+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
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.interfaces.ItemWithSubtitle
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
+import org.mjdev.tvapp.base.ui.components.card.colorFocusBorder
 import org.mjdev.tvapp.base.ui.components.card.colorFocusGlow
 import org.mjdev.tvapp.base.ui.components.image.ImageAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
@@ -47,29 +51,28 @@ fun FocusableCard(
     item: Any? = null,
     modifier: Modifier = Modifier,
     contentScale: ContentScale = ContentScale.Crop,
-    placeHolder: Int = R.drawable.placeholder,
     scale: CardScale = CardDefaults.scale(),
     shape: CardShape = CardDefaults.shape(),
     colors: CardColors = CardDefaults.colors(),
-    border: CardBorder = CardDefaults.border(),
+    border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
     placeholder: @Composable () -> Unit = {},
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    imageRenderer: @Composable (modifier:Modifier) -> Unit = {
+    focusState: MutableState<FocusState?> = rememberFocusState(),
+    imageRenderer: @Composable (modifier: Modifier) -> Unit = {
         ImageAny(
             modifier = modifier,
             src = (item as? ItemWithImage)?.imageUrl,
             contentDescription = (item as? ItemWithDescription)?.description?.toString(),
             contentScale = contentScale,
-            placeholder = placeHolder
+            placeholder = placeholder
         )
     },
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
 ) {
-    val focusState = remember { mutableStateOf<FocusState?>(null) }
-    val isFocused: () -> Boolean = {
-        ((focusState.value?.isFocused == true) || (focusState.value?.hasFocus == true))
+    val focusState = rememberSaveable {
+        mutableStateOf<FocusState?>(null)
     }
     CompactCard(
         interactionSource = interactionSource,
@@ -77,17 +80,16 @@ fun FocusableCard(
         shape = shape,
         colors = colors,
         border = border,
-        glow = CardDefaults.colorFocusGlow(Color.Green),
+        glow = glow,
         modifier = modifier
             .onFocusChanged { state ->
                 focusState.value = state
             }
-            .touchable {
-                if (isFocused())
-                    onClick(item)
-                else
-                    onFocus(item)
-            },
+            .touchable(
+                state = focusState,
+                onClick = { onClick(item) },
+                onFocus = { onFocus(item) }
+            ),
         image = {
             imageRenderer(modifier)
         },

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

@@ -21,6 +21,7 @@ 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
@@ -60,6 +61,9 @@ fun Navigation(
     navController.addMenuItem(*menuItems.toTypedArray())
 
     if (navController.menuState.value) {
+// todo right navigation with settings
+//        ModalNavigationDrawer(
+//            drawerContent = {}) {
         NavigationDrawer(
             modifier = modifier
                 .fillMaxHeight()
@@ -76,6 +80,7 @@ fun Navigation(
                 navigationState.drawerState.setValue(state)
             }
         )
+//        }
     } else {
         content()
     }

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

@@ -18,10 +18,12 @@ import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AccountCircle
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.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.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
@@ -32,6 +34,8 @@ 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.rememberFocusState
 import org.mjdev.tvapp.base.ui.components.icon.IconAny
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
@@ -46,10 +50,11 @@ fun NavigationRow(
     icon: Any? = Icons.Default.AccountCircle,
     textColor: Color = Color.White,
     iconColor: Color = Color.White,
-    focusedColor: Color = Color.Black,
+    focusedColor: Color = Color.Green,
     unFocusedColor: Color = Color.Transparent,
     expandedWidth: Dp = 150.dp,
     contentPadding: Dp = 2.dp,
+    focusState: MutableState<FocusState?> = rememberFocusState(),
     onFocus: (id: Int) -> Unit = {},
     onClick: (id: Int) -> Unit = {},
 ) {
@@ -63,6 +68,7 @@ fun NavigationRow(
             .background(
                 if (isEdit || focused.value) focusedColor else unFocusedColor
             ),
+        focusState = focusState,
         focusedColor = focusedColor,
         unFocusedColor = unFocusedColor,
         onFocus = {

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

@@ -30,6 +30,7 @@ import androidx.tv.material3.TabDefaults
 import androidx.tv.material3.TabRow
 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.text.TextAny
 
@@ -42,10 +43,10 @@ fun <T> Tabs(
     items: List<T> = emptyList(),
     fontSize: TextUnit = 12.sp,
     itemsSpacing: Dp = 8.dp,
-    activeContentColor: Color = Color.Black,
+    activeContentColor: Color = Color.Green,
     selectedContentColor: Color = Color.White,
     focusedContentColor: Color = Color.White,
-    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    interactionSource: MutableInteractionSource = rememberMutableInteractionSource(),
     onItemClick: (index: Int) -> Unit = {}
 ) {
 

+ 49 - 51
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/complex/Title.kt

@@ -8,17 +8,18 @@
 
 package org.mjdev.tvapp.base.ui.components.complex
 
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
 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.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
@@ -30,6 +31,10 @@ 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.image.CircleImage
 import org.mjdev.tvapp.base.ui.components.text.TextAny
 
@@ -40,65 +45,58 @@ fun Title(
     fontWeight: FontWeight = FontWeight.Bold,
     fontSize: TextUnit = 24.sp,
     color: Color = Color.White,
-    focusShadowColor: Color = Color.Black,
+    focusShadowColor: Color = Color.Green,
     unfocusedShadowColor: Color = Color.Transparent,
-    shadowSize: Dp = 4.dp,
+    shadowSize: Dp = 6.dp,
     title: Any? = R.string.app_name,
+    focusState: MutableState<FocusState?> = rememberFocusState(),
+    interactionSource: MutableInteractionSource = ModifierExt.rememberMutableInteractionSource(),
     onFocus: () -> Unit = {},
     onClick: () -> Unit = {},
 ) {
-
-    val isFocused: MutableState<Boolean> = remember { mutableStateOf(false) }
-    val onTouched: (focused: Boolean) -> Unit = { focused ->
-        if (focused) onClick() else onFocus()
-    }
+    val isEdit = isEditMode()
 
     FocusableBox(
         modifier = modifier,
-        isFocused = isFocused,
-        onTouched = onTouched,
+        focusState = focusState,
+        onFocus = onFocus,
+        onClick = onClick,
+        shape = RoundedCornerShape(8.dp),
         focusedColor = Color.Transparent,
+        interactionSource = interactionSource,
     ) {
-
-        isFocused.value.also { focused ->
-
-            Row(
-                modifier = modifier.padding(8.dp, 0.dp, 0.dp, 0.dp),
-                verticalAlignment = CenterVertically,
-            ) {
-
-                CircleImage(
-                    modifier = Modifier
-                        .height((fontSize.value).dp)
-                        .aspectRatio(1f)
-                        .scale(1.3f),
-                    borderColor = Color.White,
-                    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 (focused) focusShadowColor else unfocusedShadowColor,
-                            offset = Offset(shadowSize.value, shadowSize.value),
-                            blurRadius = shadowSize.value
-                        )
-                    ),
-                    text = title ?: R.string.app_name,
-                    fontWeight = fontWeight,
-                    fontSize = fontSize,
-                    color = color
-                )
-
-            }
-
+        Row(
+            modifier = modifier.padding(8.dp, 8.dp, 8.dp, 8.dp),
+            verticalAlignment = CenterVertically,
+        ) {
+            CircleImage(
+                modifier = Modifier
+                    .height((fontSize.value).dp)
+                    .aspectRatio(1f)
+                    .scale(1.3f),
+                borderColor = Color.White,
+                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
+                    )
+                ),
+                text = title ?: R.string.app_name,
+                fontWeight = fontWeight,
+                fontSize = fontSize,
+                color = color
+            )
         }
 
     }

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

@@ -11,7 +11,9 @@ package org.mjdev.tvapp.base.ui.components.complex
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.runtime.Composable
+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.graphics.Shape
 import androidx.compose.ui.tooling.preview.Preview
@@ -20,6 +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.image.CircleImage
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -33,9 +37,11 @@ fun UserPic(
     borderColor: Color = Color.White,
     borderSize: Dp = 2.dp,
     contentPadding: Dp = 2.dp,
+    focusState: MutableState<FocusState?> = rememberFocusState(),
     onClick: () -> Unit = {}
 ) = FocusableBox(
     modifier = modifier.wrapContentSize(),
+    focusState = focusState,
     shape = shape,
     onClick = onClick
 ) {

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

@@ -26,10 +26,8 @@ import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.tooling.preview.Preview
-import androidx.core.content.ContextCompat
 import coil.compose.AsyncImage
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.extensions.DrawableExt.asImageBitmap
@@ -47,7 +45,7 @@ fun ImageAny(
     contentScale: ContentScale = ContentScale.Fit,
     alpha: Float = DefaultAlpha,
     colorFilter: ColorFilter? = null,
-    placeholder: Int? = null
+    placeholder: @Composable () -> Unit = {}, // todo
 ) = BoxWithConstraints(
     modifier = modifier
 ) {
@@ -55,23 +53,9 @@ fun ImageAny(
     val width = if(constraints.minWidth  ==0) 1 else constraints.minWidth
     val height = if(constraints.minHeight  ==0) 1 else constraints.minHeight
 
-    val context = LocalContext.current
-
-    val placeholderDrawable : Drawable = (placeholder?.let { ph ->
-        ContextCompat.getDrawable(context, ph)
-    } ?: ColorDrawable(0))
-
     when (src) {
 
-        null -> Image(
-            placeholderDrawable.asImageBitmap(width, height),
-            contentDescription,
-            modifier,
-            alignment,
-            contentScale,
-            alpha,
-            colorFilter
-        )
+        null -> placeholder()
 
         Color -> Image(
             ColorDrawable((src as Color).toArgb()).asImageBitmap(width, height),

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

@@ -52,9 +52,9 @@ open class Page {
             Modifier
                 .fillMaxSize()
                 .background(backgroundColor, background)
-                .touchable {
-                    freeFocus()
-                }
+//                .touchable {
+//                    freeFocus()
+//                }
         ) {
             TouchBox(
                 contentAlignment = Alignment.Center,

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

@@ -30,17 +30,17 @@ 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.ModifierExt.touchable
 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.ItemCard
+import org.mjdev.tvapp.base.ui.components.card.PhotoCard
 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
@@ -58,6 +58,7 @@ 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() {
@@ -124,7 +125,7 @@ class MainPage : Page() {
             }
             if (isEdit || featuredMovieList.value.isNotEmpty()) item {
                 BigCarousel(
-                    modifier = Modifier.touchable(),
+                    modifier = Modifier,
                     items = featuredMovieList.value,
                     onItemClicked = onItemClick
                 )
@@ -149,7 +150,7 @@ class MainPage : Page() {
                     )
                 ) {
                     items(category.movieList) { item ->
-                        ItemCard(
+                        PhotoCard(
                             item = item,
                             onClick = onItemClick
                         )