Просмотр исходного кода

detail improvements

detail improvements

detail improvements

detail improvements

detail improvements

detail improvements

detail improvements

detail improvements

detail improvements
Milan Jurkulak 3 лет назад
Родитель
Сommit
46469c4309
40 измененных файлов с 1131 добавлено и 212 удалено
  1. 8 4
      app/src/main/AndroidManifest.xml
  2. 18 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Movie.kt
  3. 24 100
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  4. 35 8
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt
  5. 62 0
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/IPTVViewModel.kt
  6. BIN
      app/src/main/res/drawable/placeholder.png
  7. 2 0
      tvlib/build.gradle.kts
  8. 11 7
      tvlib/src/main/AndroidManifest.xml
  9. 2 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt
  10. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt
  11. 30 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt
  12. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/StringExt.kt
  13. 2 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/brush/BrushBuilder.kt
  14. 31 11
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioItem.kt
  15. 47 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursor.kt
  16. 38 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursorListIterator.kt
  17. 24 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoItem.kt
  18. 31 11
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoItem.kt
  19. 37 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/other/Observed.kt
  20. 13 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithDate.kt
  21. 7 6
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/button/Button.kt
  22. 6 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/ItemCard.kt
  23. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/PhotoCard.kt
  24. 0 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/CarouselCard.kt
  25. 13 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/complex/FocusableCard.kt
  26. 217 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt
  27. 127 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/ItemInfo.kt
  28. 2 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/FadingPhotoImage.kt
  29. 30 10
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/ImageAny.kt
  30. 57 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/ImageBackground.kt
  31. 28 13
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/PhotoImage.kt
  32. 62 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/immersivelist/ImmersiveList.kt
  33. 70 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/immersivelist/ImmersiveListBackgroundScope.kt
  34. 30 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/immersivelist/ImmersiveListScope.kt
  35. 3 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/text/AutoHideEmptyText.kt
  36. 22 16
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/ErrorMessage.kt
  37. 0 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/TVRow.kt
  38. 18 0
      tvlib/src/main/res/drawable/broken_image.xml
  39. BIN
      tvlib/src/main/res/drawable/person.png
  40. 12 0
      tvlib/src/main/res/drawable/person.xml

+ 8 - 4
app/src/main/AndroidManifest.xml

@@ -15,16 +15,18 @@
 
 
 
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
 
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
     <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
 
 
-    <uses-permission
-        android:name="android.permission.READ_EXTERNAL_STORAGE"
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
         tools:ignore="ScopedStorage" />
         tools:ignore="ScopedStorage" />
     <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
     <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
     <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
     <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
 
 
     <uses-permission
     <uses-permission
         android:name="android.permission.AUTHENTICATE_ACCOUNTS"
         android:name="android.permission.AUTHENTICATE_ACCOUNTS"
@@ -97,8 +99,9 @@
 
 
             <intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.APP_BROWSER"/>
-                <category android:name="android.intent.category.DEFAULT"/>
+
+                <category android:name="android.intent.category.APP_BROWSER" />
+                <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
             </intent-filter>
 
 
             <intent-filter>
             <intent-filter>
@@ -113,6 +116,7 @@
 
 
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.BROWSABLE" />
                 <category android:name="android.intent.category.BROWSABLE" />
+
                 <data android:scheme="http" />
                 <data android:scheme="http" />
                 <data android:scheme="https" />
                 <data android:scheme="https" />
             </intent-filter>
             </intent-filter>

+ 18 - 0
app/src/main/kotlin/org/mjdev/tvapp/data/local/Movie.kt

@@ -76,4 +76,22 @@ class Movie :
             studio = value
             studio = value
         }
         }
 
 
+    override fun equals(other: Any?): Boolean {
+        if (other == null || other !is Movie) return false
+        return uri == other.uri
+    }
+
+    override fun hashCode(): Int {
+        var result = id.hashCode()
+        result = 31 * result + (category?.hashCode() ?: 0)
+        result = 31 * result + (studio?.hashCode() ?: 0)
+        result = 31 * result + (country?.hashCode() ?: 0)
+        result = 31 * result + isNsfw.hashCode()
+        result = 31 * result + (title?.hashCode() ?: 0)
+        result = 31 * result + (description?.hashCode() ?: 0)
+        result = 31 * result + (uri?.hashCode() ?: 0)
+        result = 31 * result + (subtitle?.hashCode() ?: 0)
+        return result
+    }
+
 }
 }

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

@@ -6,50 +6,28 @@
  * w: https://mjdev.org
  * w: https://mjdev.org
  */
  */
 
 
+@file:Suppress("MoveVariableDeclarationIntoWhen")
+
 package org.mjdev.tvapp.ui.screens
 package org.mjdev.tvapp.ui.screens
 
 
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Info
 import androidx.compose.material.icons.filled.Info
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.ContentScale
 import androidx.navigation.navArgument
 import androidx.navigation.navArgument
-import kotlinx.coroutines.delay
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.R
+import org.mjdev.tvapp.data.local.Movie
+import org.mjdev.tvapp.viewmodel.IPTVViewModel
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.extensions.HiltExt.appViewModel
+import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemPhoto
-import org.mjdev.tvlib.interfaces.ItemWithBackground
-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.interfaces.ItemVideo
 import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.screen.Screen
 import org.mjdev.tvlib.screen.Screen
-import org.mjdev.tvlib.ui.components.image.FadingPhotoImage
-import org.mjdev.tvlib.ui.components.image.ImageAny
-import org.mjdev.tvlib.ui.components.text.AutoHideEmptyText
-import org.mjdev.tvlib.ui.theme.ThemeHelper.themeProvider
+import org.mjdev.tvlib.ui.components.gallery.Gallery
 
 
 class DetailScreen : Screen() {
 class DetailScreen : Screen() {
 
 
@@ -72,80 +50,26 @@ class DetailScreen : Screen() {
     @Composable
     @Composable
     override fun ComposeScreen() {
     override fun ComposeScreen() {
 
 
+        val viewModel: IPTVViewModel = appViewModel { context ->
+            IPTVViewModel.mockIPTVViewModel(context)
+        }
+
         val data: Any? = args[data]
         val data: Any? = args[data]
 
 
-        val photo = (data as? ItemPhoto)?.uri
-        val backgroundImage = (data as? ItemWithBackground<*>)?.background
-        val image = (data as? ItemWithImage<*>)?.image
-        val title = (data as? ItemWithTitle<*>)?.title
-        val subtitle = (data as? ItemWithSubtitle<*>)?.subtitle
-        val description = (data as? ItemWithDescription<*>)?.description
-        var infoVisible by remember { mutableStateOf(true) }
-
-        Box(
-            modifier = Modifier
-                .fillMaxSize()
-                .pointerInput(Unit) {
-                    detectTapGestures {
-                        infoVisible = !infoVisible
-                    }
-                },
-            contentAlignment = Alignment.BottomCenter,
-        ) {
-            FadingPhotoImage(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .background(Color.Black, RectangleShape),
-                initialImage = backgroundImage ?: photo ?: image,
-                contrast = 4f,
-                alpha = 0.6f,
-                brightness = -255f,
-                contentScale = ContentScale.Crop
-            )
-            ImageAny(
-                src = photo ?: backgroundImage ?: image,
-                contentDescription = null,
-                contentScale = ContentScale.Fit,
-                modifier = Modifier.fillMaxSize()
-            )
-            AnimatedVisibility(
-                visible = infoVisible,
-                enter = fadeIn(),
-                exit = fadeOut(),
-            ) {
-                Column(
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .alpha(0.8f)
-                        .background(themeProvider.detailTextBackgroundColor)
-                        .padding(
-                            vertical = themeProvider.detailTextPaddingVertical,
-                            horizontal = themeProvider.detailTextPaddingHorizontal
-                        )
-                ) {
-                    AutoHideEmptyText(
-                        text = title,
-                        style = themeProvider.detailsTitleTextStyle,
-                        color = themeProvider.detailsTextColor
-                    )
-                    AutoHideEmptyText(
-                        text = subtitle,
-                        style = themeProvider.detailsTextStyle,
-                        color = themeProvider.detailsTextColor
-                    )
-                    AutoHideEmptyText(
-                        text = description,
-                        style = themeProvider.detailsTextStyle,
-                        color = themeProvider.detailsTextColor
-                    )
-                }
-            }
-            LaunchedEffect(infoVisible) {
-                delay(2000)
-                infoVisible = false
-            }
+        val dataList: List<Any?> = when (data) {
+            is ItemAudio -> viewModel.localAudioCursor
+            is ItemVideo -> viewModel.localVideoCursor
+            is ItemPhoto -> viewModel.localPhotoCursor
+            is Movie -> viewModel.movieList()
+            else -> listOf(data)
         }
         }
 
 
+        Gallery (
+            modifier = Modifier.fillMaxSize(),
+            list = dataList,
+            index = dataList.indexOf(data)
+        )
+
     }
     }
 
 
 }
 }

+ 35 - 8
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Tv
 import androidx.compose.material.icons.filled.Tv
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
@@ -22,8 +23,14 @@ import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.navigation.navArgument
 import androidx.navigation.navArgument
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.R
+import org.mjdev.tvapp.data.local.Movie
+import org.mjdev.tvapp.viewmodel.IPTVViewModel
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.extensions.HiltExt.appViewModel
 import org.mjdev.tvlib.extensions.StringExt.parseUri
 import org.mjdev.tvlib.extensions.StringExt.parseUri
+import org.mjdev.tvlib.interfaces.ItemAudio
+import org.mjdev.tvlib.interfaces.ItemPhoto
+import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithUri
 import org.mjdev.tvlib.interfaces.ItemWithUri
 import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.navigation.AnyType
@@ -51,19 +58,39 @@ class IPTVScreen : Screen() {
     @Composable
     @Composable
     override fun ComposeScreen() {
     override fun ComposeScreen() {
 
 
+        val viewModel: IPTVViewModel = appViewModel { context ->
+            IPTVViewModel.mockIPTVViewModel(context)
+        }
+
         val data: Any? = remember { args[data] }
         val data: Any? = remember { args[data] }
 
 
-        val mediaUri: String? = when (data) {
+        val dataList = remember {
+            when (data) {
+                is ItemAudio -> viewModel.localAudioCursor.asList()
+                is ItemVideo -> viewModel.localVideoCursor.asList()
+                is ItemPhoto -> viewModel.localPhotoCursor.asList()
+                is Movie -> viewModel.movieList()
+                else -> listOf(data)
+            }
+        }
+
+        val currentIndex = remember {
+            mutableIntStateOf(dataList.indexOf(data))
+        }
+
+        val currentData = dataList[currentIndex.intValue]
+
+        val mediaUri: String? = when (currentData) {
             null -> null
             null -> null
-            is ItemWithUri<*> -> data.uri.toString()
-            is String -> data.toString()
-            is URL -> data.toString()
-            is Uri -> data.toString()
-            else -> data.toString()
+            is ItemWithUri<*> -> currentData.uri.toString()
+            is String -> currentData.toString()
+            is URL -> currentData.toString()
+            is Uri -> currentData.toString()
+            else -> currentData.toString()
         }
         }
 
 
-        val mediaTitle: String? = when (data) {
-            is ItemWithTitle<*> -> data.title.toString()
+        val mediaTitle: String? = when (currentData) {
+            is ItemWithTitle<*> -> currentData.title.toString()
             else -> null
             else -> null
         }
         }
 
 

+ 62 - 0
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/IPTVViewModel.kt

@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) Milan Jurkulák 2023.
+ * Contact:
+ * e: mimoccc@gmail.com
+ * e: mj@mjdev.org
+ * w: https://mjdev.org
+ */
+
+package org.mjdev.tvapp.viewmodel
+
+import android.content.Context
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.runBlocking
+import org.mjdev.tvlib.helpers.cursor.AudioCursor
+import org.mjdev.tvlib.helpers.cursor.PhotoCursor
+import org.mjdev.tvlib.helpers.cursor.VideoCursor
+import org.mjdev.tvlib.network.NetworkConnectivityService
+import org.mjdev.tvlib.viewmodel.BaseViewModel
+import org.mjdev.tvapp.database.DAO
+import org.mjdev.tvapp.repository.IMovieRepository
+import org.mjdev.tvapp.repository.MovieRepository
+import org.mjdev.tvlib.network.NetworkConnectivityServiceImpl
+import javax.inject.Inject
+
+@HiltViewModel
+class IPTVViewModel @Inject constructor() : BaseViewModel() {
+
+    @Inject
+    lateinit var movieRepository: IMovieRepository
+
+    @Inject
+    lateinit var networkInfo: NetworkConnectivityService
+
+    @Inject
+    lateinit var localAudioCursor: AudioCursor
+
+    @Inject
+    lateinit var localVideoCursor: VideoCursor
+
+    @Inject
+    lateinit var localPhotoCursor: PhotoCursor
+
+    fun movieList() = runBlocking {
+        movieRepository.getMovies().getOrNull() ?: emptyList()
+    }
+
+    companion object {
+
+        @Suppress("unused")
+        fun mockIPTVViewModel(
+            context: Context
+        ): IPTVViewModel = IPTVViewModel().apply {
+            movieRepository = MovieRepository(DAO(context))
+            networkInfo = NetworkConnectivityServiceImpl(context)
+            localAudioCursor = AudioCursor(context)
+            localVideoCursor = VideoCursor(context)
+            localPhotoCursor = PhotoCursor(context)
+        }
+
+    }
+
+}

BIN
app/src/main/res/drawable/placeholder.png


+ 2 - 0
tvlib/build.gradle.kts

@@ -154,6 +154,8 @@ dependencies {
     implementation("androidx.palette:palette-ktx:1.0.0")
     implementation("androidx.palette:palette-ktx:1.0.0")
     // lottie
     // lottie
     implementation("com.airbnb.android:lottie-compose:6.1.0")
     implementation("com.airbnb.android:lottie-compose:6.1.0")
+    // exif info
+    implementation("androidx.exifinterface:exifinterface:1.3.6")
     // constraints
     // constraints
     constraints {
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0")?.apply {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0")?.apply {

+ 11 - 7
tvlib/src/main/AndroidManifest.xml

@@ -1,17 +1,16 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ~ Copyright (c) Milan Jurkulák 2023.
   ~ Copyright (c) Milan Jurkulák 2023.
   ~  Contact:
   ~  Contact:
   ~  e: mimoccc@gmail.com
   ~  e: mimoccc@gmail.com
   ~  e: mj@mjdev.org
   ~  e: mj@mjdev.org
   ~  w: https://mjdev.org
   ~  w: https://mjdev.org
   -->
   -->
+<manifest xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android">
 
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
-<!--    <permission-->
-<!--        android:name="android.permission.QUERY_ALL_PACKAGES"-->
-<!--        tools:ignore="ReservedSystemPermission" />-->
+    <!--    <permission-->
+    <!--        android:name="android.permission.QUERY_ALL_PACKAGES"-->
+    <!--        tools:ignore="ReservedSystemPermission" />-->
 
 
     <queries>
     <queries>
         <intent>
         <intent>
@@ -19,4 +18,9 @@
         </intent>
         </intent>
     </queries>
     </queries>
 
 
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+
 </manifest>
 </manifest>

+ 2 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt

@@ -64,11 +64,12 @@ object ComposeExt {
         get() = currentValue == DrawerValue.Closed
         get() = currentValue == DrawerValue.Closed
 
 
     @Composable
     @Composable
-    inline fun <reified T> textFrom(text: T?): String = when (text) {
+    fun <T> textFrom(text: T?): String = when (text) {
         null -> ""
         null -> ""
         is Unit -> ""
         is Unit -> ""
         is Int -> LocalContext.current.getString(text)
         is Int -> LocalContext.current.getString(text)
         is String -> text
         is String -> text
+        is MutableState<*> -> textFrom(text.value)
         else -> text.toString()
         else -> text.toString()
     }
     }
 
 

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt

@@ -9,11 +9,16 @@
 package org.mjdev.tvlib.extensions
 package org.mjdev.tvlib.extensions
 
 
 import android.os.Handler
 import android.os.Handler
+import androidx.compose.runtime.MutableState
 import timber.log.Timber
 import timber.log.Timber
 
 
 @Suppress("unused")
 @Suppress("unused")
 object GlobalExt {
 object GlobalExt {
 
 
+    fun MutableState<Boolean>.toggle() {
+        value = !value
+    }
+
     suspend fun <E> runSafe(block: suspend () -> E): Result<E> = try {
     suspend fun <E> runSafe(block: suspend () -> E): Result<E> = try {
         block.invoke().let { result ->
         block.invoke().let { result ->
             Result.success(result)
             Result.success(result)

+ 30 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt

@@ -9,8 +9,11 @@
 package org.mjdev.tvlib.extensions
 package org.mjdev.tvlib.extensions
 
 
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.Stable
@@ -24,6 +27,7 @@ import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.SolidColor
@@ -31,10 +35,10 @@ import androidx.compose.ui.graphics.drawscope.Fill
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.lerp
 import androidx.compose.ui.graphics.lerp
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
-import org.mjdev.tvlib.BuildConfig
 import timber.log.Timber
 import timber.log.Timber
 import java.lang.Float.min
 import java.lang.Float.min
 
 
@@ -42,7 +46,7 @@ import java.lang.Float.min
 object ModifierExt {
 object ModifierExt {
 
 
     // used to debug library
     // used to debug library
-    val isDebug = false
+    const val isDebug = false
 
 
     fun Modifier.conditional(
     fun Modifier.conditional(
         condition: Boolean,
         condition: Boolean,
@@ -148,4 +152,28 @@ object ModifierExt {
         }
         }
     }
     }
 
 
+    @OptIn(ExperimentalFoundationApi::class)
+    fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
+        inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
+        factory = {
+            var myRect: Rect = Rect.Zero
+            this
+                .onSizeChanged {
+                    myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
+                }
+                .bringIntoViewResponder(
+                    remember {
+                        object : BringIntoViewResponder {
+                            @ExperimentalFoundationApi
+                            override fun calculateRectForParent(localRect: Rect): Rect = myRect
+
+                            @ExperimentalFoundationApi
+                            override suspend fun bringChildIntoView(localRect: () -> Rect?) {
+                            }
+                        }
+                    }
+                )
+        }
+    )
+
 }
 }

+ 1 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/StringExt.kt

@@ -14,6 +14,6 @@ object StringExt {
 
 
     fun String.asException() = Exception(this)
     fun String.asException() = Exception(this)
 
 
-    fun String.parseUri() = Uri.parse(this)
+    fun String.parseUri(): Uri = Uri.parse(this)
 
 
 }
 }

+ 2 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/brush/BrushBuilder.kt

@@ -28,7 +28,7 @@ class BrushBuilder {
     private var end: Float = Float.POSITIVE_INFINITY
     private var end: Float = Float.POSITIVE_INFINITY
     private var tileMode: TileMode = TileMode.Clamp
     private var tileMode: TileMode = TileMode.Clamp
 
 
-    fun build(): Brush? = when (type) {
+    fun build(): Brush = when (type) {
         Vertical -> {
         Vertical -> {
             Brush.verticalGradient(
             Brush.verticalGradient(
                 colorStops = colorStops,
                 colorStops = colorStops,
@@ -38,7 +38,7 @@ class BrushBuilder {
             )
             )
         }
         }
 
 
-        Horizontal -> {
+        else -> {
             Brush.horizontalGradient(
             Brush.horizontalGradient(
                 colorStops = colorStops,
                 colorStops = colorStops,
                 startX = start,
                 startX = start,
@@ -46,8 +46,6 @@ class BrushBuilder {
                 tileMode = tileMode
                 tileMode = tileMode
             )
             )
         }
         }
-
-        else -> null
     }
     }
 
 
     fun type(type: Int): BrushBuilder = apply {
     fun type(type: Int): BrushBuilder = apply {

+ 31 - 11
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioItem.kt

@@ -9,13 +9,17 @@
 package org.mjdev.tvlib.helpers.cursor
 package org.mjdev.tvlib.helpers.cursor
 
 
 import android.database.Cursor
 import android.database.Cursor
+import android.media.MediaMetadataRetriever
 import android.net.Uri
 import android.net.Uri
 import android.provider.MediaStore
 import android.provider.MediaStore
 import org.mjdev.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemAudio
+import org.mjdev.tvlib.interfaces.ItemWithDate
+import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithUri
 import org.mjdev.tvlib.interfaces.ItemWithUri
+import timber.log.Timber
 import java.io.Serializable
 import java.io.Serializable
 
 
 @Suppress("unused")
 @Suppress("unused")
@@ -24,28 +28,46 @@ class AudioItem() :
     ItemAudio,
     ItemAudio,
     ItemWithTitle<String>,
     ItemWithTitle<String>,
     ItemWithImage<Any>,
     ItemWithImage<Any>,
-    ItemWithUri<String> {
+    ItemWithUri<String>,
+    ItemWithDescription<String?>,
+    ItemWithDate {
 
 
     override var title: String? = null
     override var title: String? = null
-
     override var uri: String? = null
     override var uri: String? = null
-
     override var image: Any? = null
     override var image: Any? = null
+    override var date: String? = null
+    override var description: String? = null
 
 
     constructor(c: Cursor) : this() {
     constructor(c: Cursor) : this() {
         c.asMap(VideoItem.MEDIA_PROJECTION).forEach { vp ->
         c.asMap(VideoItem.MEDIA_PROJECTION).forEach { vp ->
             when (vp.key) {
             when (vp.key) {
                 MediaStore.Audio.Media.TITLE -> title = vp.value.toString()
                 MediaStore.Audio.Media.TITLE -> title = vp.value.toString()
-                MediaStore.Audio.Media._ID -> uri = Uri.withAppendedPath(
-                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
-                    vp.value.toString()
-                ).toString()
-
-                MediaStore.Video.Media.DATA -> image = vp.value.toString()
+                MediaStore.Audio.Media.DATA -> {
+                    uri = vp.value.toString()
+                    image = vp.value.toString()
+                }
+            }
+        }
+        try {
+            MediaMetadataRetriever().apply {
+                setDataSource(uri)
+            }.also { mmr ->
+                date = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)
             }
             }
+        } catch (e: Exception) {
+            Timber.e(e)
         }
         }
     }
     }
 
 
+    override fun equals(other: Any?): Boolean {
+        if (other == null || other !is AudioItem) return false
+        return uri == other.uri
+    }
+
+    override fun hashCode(): Int {
+        return uri?.hashCode() ?: 0
+    }
+
     companion object {
     companion object {
 
 
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Audio.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Audio.Media.DATE_ADDED + " DESC"
@@ -57,8 +79,6 @@ class AudioItem() :
             MediaStore.Audio.Media._ID,
             MediaStore.Audio.Media._ID,
             MediaStore.Audio.Media.TITLE,
             MediaStore.Audio.Media.TITLE,
             MediaStore.Audio.Media.DATA,
             MediaStore.Audio.Media.DATA,
-            MediaStore.Audio.Media.DATE_ADDED,
-            // todo more columns
         )
         )
 
 
     }
     }

+ 47 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursor.kt

@@ -40,9 +40,9 @@ open class CachingCursor(
     selectionArgs: Array<String>? = null,
     selectionArgs: Array<String>? = null,
     sortOrder: String? = null,
     sortOrder: String? = null,
     private val prefetchItems: Int = 8,
     private val prefetchItems: Int = 8,
-    private val cacheItems: Int = 256,
+    private var cacheItems: Int = -1,
     private val transform: (Cursor) -> Any? = { it },
     private val transform: (Cursor) -> Any? = { it },
-) : Cursor {
+) : Cursor, List<Any?> {
 
 
     private val resolver: ContentResolver by lazy {
     private val resolver: ContentResolver by lazy {
         context.contentResolver
         context.contentResolver
@@ -75,6 +75,9 @@ open class CachingCursor(
     val isNotEmpty: Boolean get() = count > 0
     val isNotEmpty: Boolean get() = count > 0
 
 
     init {
     init {
+        if (cacheItems == -1) {
+            cacheItems = cursor?.count ?: 0
+        }
         (0..prefetchItems).forEach { idx -> getData(idx) }
         (0..prefetchItems).forEach { idx -> getData(idx) }
     }
     }
 
 
@@ -282,6 +285,48 @@ open class CachingCursor(
     override fun respond(b: Bundle?): Bundle? =
     override fun respond(b: Bundle?): Bundle? =
         cursor?.respond(b)
         cursor?.respond(b)
 
 
+    override val size: Int
+        get() = count
+
+    override fun get(index: Int): Any? = _get(index)
+    override fun isEmpty(): Boolean = (size == 0)
+
+    override fun iterator(): Iterator<Any?> =
+        CachingCursorListIterator(this)
+
+    override fun listIterator(): ListIterator<Any?> =
+        CachingCursorListIterator(this)
+
+    override fun listIterator(index: Int): ListIterator<Any?> =
+        CachingCursorListIterator(this, index)
+
+    override fun subList(fromIndex: Int, toIndex: Int): List<Any?> =
+        (fromIndex..toIndex).map { idx -> get(idx) }
+
+    override fun lastIndexOf(element: Any?): Int {
+        var curr: Int = size - 1
+        while ((this[curr] != element) && (curr > 0)) {
+            curr--
+        }
+        return curr
+    }
+
+    override fun indexOf(element: Any?): Int {
+        var curr = 0
+        while ((this[curr] != element) && (curr < size)) {
+            curr++
+        }
+        return if (curr == size) -1 else curr
+    }
+
+    override fun containsAll(elements: Collection<Any?>): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun contains(element: Any?): Boolean {
+        TODO("Not yet implemented")
+    }
+
     data class CachedCursorItem(
     data class CachedCursorItem(
         val idx: Int,
         val idx: Int,
         val cached: Long,
         val cached: Long,

+ 38 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/CachingCursorListIterator.kt

@@ -0,0 +1,38 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.helpers.cursor
+
+@Suppress("CanBeParameter")
+class CachingCursorListIterator(
+    private val cachingCursor: CachingCursor,
+    private val index: Int = 0
+) : ListIterator<Any?> {
+
+    private var idx = index
+
+    override fun hasNext(): Boolean = cachingCursor.size > (idx - 1)
+
+    override fun hasPrevious(): Boolean = idx > -1
+
+    override fun next(): Any? {
+        val ret = cachingCursor[idx]
+        idx = nextIndex()
+        return ret
+    }
+
+    override fun nextIndex(): Int = idx + 1
+
+    override fun previous(): Any? {
+        idx = previousIndex()
+        return cachingCursor[idx]
+    }
+
+    override fun previousIndex(): Int = if ((idx - 1) < 0) 0 else idx - 1
+
+}

+ 24 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoItem.kt

@@ -11,11 +11,15 @@ package org.mjdev.tvlib.helpers.cursor
 import android.database.Cursor
 import android.database.Cursor
 import android.net.Uri
 import android.net.Uri
 import android.provider.MediaStore
 import android.provider.MediaStore
+import androidx.exifinterface.media.ExifInterface
 import org.mjdev.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithBackground
+import org.mjdev.tvlib.interfaces.ItemWithDate
+import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithTitle
+import timber.log.Timber
 import java.io.Serializable
 import java.io.Serializable
 
 
 @Suppress("unused")
 @Suppress("unused")
@@ -24,12 +28,16 @@ class PhotoItem() :
     ItemPhoto,
     ItemPhoto,
     ItemWithTitle<String>,
     ItemWithTitle<String>,
     ItemWithImage<Any>,
     ItemWithImage<Any>,
-    ItemWithBackground<Any?> {
+    ItemWithBackground<Any?>,
+    ItemWithDescription<String?>,
+    ItemWithDate {
 
 
     override var title: String? = null
     override var title: String? = null
     override var uri: String? = null
     override var uri: String? = null
     override var image: Any? = null
     override var image: Any? = null
     override var background: Any? = null
     override var background: Any? = null
+    override var date: String? = null
+    override var description: String? = null
 
 
     constructor(c: Cursor) : this() {
     constructor(c: Cursor) : this() {
         c.asMap(VideoItem.MEDIA_PROJECTION).forEach { vp ->
         c.asMap(VideoItem.MEDIA_PROJECTION).forEach { vp ->
@@ -41,11 +49,26 @@ class PhotoItem() :
                 ).toString()
                 ).toString()
 
 
                 MediaStore.Images.Media.DATA -> {
                 MediaStore.Images.Media.DATA -> {
+                    uri = vp.value.toString()
                     image = vp.value.toString()
                     image = vp.value.toString()
                     background = vp.value.toString()
                     background = vp.value.toString()
                 }
                 }
             }
             }
         }
         }
+        try {
+            date = ExifInterface(image.toString()).getAttribute(ExifInterface.TAG_DATETIME)
+        } catch (e: Exception) {
+            Timber.e(e)
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other == null || other !is PhotoItem) return false
+        return uri == other.uri
+    }
+
+    override fun hashCode(): Int {
+        return uri?.hashCode() ?: 0
     }
     }
 
 
     companion object {
     companion object {
@@ -59,8 +82,6 @@ class PhotoItem() :
             MediaStore.Images.Media._ID,
             MediaStore.Images.Media._ID,
             MediaStore.Images.Media.TITLE,
             MediaStore.Images.Media.TITLE,
             MediaStore.Images.Media.DATA,
             MediaStore.Images.Media.DATA,
-            MediaStore.Images.Media.DATE_ADDED,
-            // todo more columns
         )
         )
 
 
     }
     }

+ 31 - 11
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoItem.kt

@@ -9,13 +9,17 @@
 package org.mjdev.tvlib.helpers.cursor
 package org.mjdev.tvlib.helpers.cursor
 
 
 import android.database.Cursor
 import android.database.Cursor
+import android.media.MediaMetadataRetriever
 import android.net.Uri
 import android.net.Uri
 import android.provider.MediaStore
 import android.provider.MediaStore
 import org.mjdev.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.interfaces.ItemVideo
+import org.mjdev.tvlib.interfaces.ItemWithDate
+import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithUri
 import org.mjdev.tvlib.interfaces.ItemWithUri
+import timber.log.Timber
 import java.io.Serializable
 import java.io.Serializable
 
 
 @Suppress("unused")
 @Suppress("unused")
@@ -24,28 +28,46 @@ class VideoItem() :
     ItemVideo,
     ItemVideo,
     ItemWithTitle<String>,
     ItemWithTitle<String>,
     ItemWithImage<Any>,
     ItemWithImage<Any>,
-    ItemWithUri<String> {
+    ItemWithUri<String>,
+    ItemWithDescription<String?>,
+    ItemWithDate {
 
 
     override var title: String? = null
     override var title: String? = null
-
     override var uri: String? = null
     override var uri: String? = null
-
     override var image: Any? = null
     override var image: Any? = null
+    override var date: String? = null
+    override var description: String? = null
 
 
     constructor(c: Cursor) : this() {
     constructor(c: Cursor) : this() {
         c.asMap(MEDIA_PROJECTION).forEach { vp ->
         c.asMap(MEDIA_PROJECTION).forEach { vp ->
             when (vp.key) {
             when (vp.key) {
                 MediaStore.Video.Media.TITLE -> title = vp.value.toString()
                 MediaStore.Video.Media.TITLE -> title = vp.value.toString()
-                MediaStore.Video.Media._ID -> uri = Uri.withAppendedPath(
-                    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-                    vp.value.toString()
-                ).toString()
-
-                MediaStore.Video.Media.DATA -> image = vp.value.toString()
+                MediaStore.Video.Media.DATA -> {
+                    uri = vp.value.toString()
+                    image = vp.value.toString()
+                }
+            }
+        }
+        try {
+            MediaMetadataRetriever().apply {
+                setDataSource(uri)
+            }.also { mmr ->
+                date = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)
             }
             }
+        } catch (e: Exception) {
+            Timber.e(e)
         }
         }
     }
     }
 
 
+    override fun equals(other: Any?): Boolean {
+        if (other == null || other !is VideoItem) return false
+        return uri == other.uri
+    }
+
+    override fun hashCode(): Int {
+        return uri?.hashCode() ?: 0
+    }
+
     companion object {
     companion object {
 
 
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Video.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Video.Media.DATE_ADDED + " DESC"
@@ -57,8 +79,6 @@ class VideoItem() :
             MediaStore.Video.Media._ID,
             MediaStore.Video.Media._ID,
             MediaStore.Video.Media.TITLE,
             MediaStore.Video.Media.TITLE,
             MediaStore.Video.Media.DATA,
             MediaStore.Video.Media.DATA,
-            MediaStore.Video.Media.DATE_ADDED
-            // todo more columns
         )
         )
 
 
     }
     }

+ 37 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/other/Observed.kt

@@ -0,0 +1,37 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+@file:Suppress("unused")
+
+package org.mjdev.tvlib.helpers.other
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+open class Observed<T>(
+    initial: T
+) : MutableState<T> {
+
+    private var _value by mutableStateOf(initial)
+
+    override var value: T = initial
+        get() = _value
+        set(value) {
+            _value = value
+            field = value
+        }
+
+    override fun component1(): T = value
+
+    override fun component2(): (T) -> Unit = {
+        value = it
+    }
+
+}

+ 13 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/interfaces/ItemWithDate.kt

@@ -0,0 +1,13 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.interfaces
+
+interface ItemWithDate {
+    var date: String?
+}

+ 7 - 6
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/button/Button.kt

@@ -11,6 +11,7 @@ package org.mjdev.tvlib.ui.components.button
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.border
 import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -44,7 +45,6 @@ import org.mjdev.tvlib.ui.components.text.TextAny
 @Composable
 @Composable
 fun Button(
 fun Button(
     text: Any? = "test",
     text: Any? = "test",
-    onClick: () -> Unit = {},
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
     fontSize: TextUnit = 24.sp,
     fontSize: TextUnit = 24.sp,
     fontWeight: FontWeight = FontWeight.Bold,
     fontWeight: FontWeight = FontWeight.Bold,
@@ -60,16 +60,17 @@ fun Button(
     focusState: MutableState<FocusState?> = rememberFocusState(text),
     focusState: MutableState<FocusState?> = rememberFocusState(text),
     content: @Composable RowScope.() -> Unit = {
     content: @Composable RowScope.() -> Unit = {
         TextAny(text = text)
         TextAny(text = text)
-    }
+    },
+    onClick: () -> Unit = {},
 ) {
 ) {
 
 
     Button(
     Button(
         modifier = modifier
         modifier = modifier
             .recomposeHighlighter()
             .recomposeHighlighter()
-//            .touchable(
-//                focusState = focusState,
-//            )
-            .border(BorderStroke(borderSize, borderColor), shape),
+            .border(BorderStroke(borderSize, borderColor), shape)
+            .clickable {
+               onClick()
+            },
         glow = glow,
         glow = glow,
         scale = scale,
         scale = scale,
         onClick = onClick,
         onClick = onClick,

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

@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.ContentScale
@@ -28,6 +29,7 @@ import androidx.tv.material3.CardScale
 import androidx.tv.material3.CardShape
 import androidx.tv.material3.CardShape
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithDescription
@@ -49,7 +51,6 @@ fun ItemCard(
     colors: CardColors = CardDefaults.colors(),
     colors: CardColors = CardDefaults.colors(),
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
-    placeholder: @Composable () -> Unit = {},
     aspectRatio: Float = 16f / 9f,
     aspectRatio: Float = 16f / 9f,
     imageRenderer: @Composable () -> Unit = {
     imageRenderer: @Composable () -> Unit = {
         ImageAny(
         ImageAny(
@@ -57,11 +58,12 @@ fun ItemCard(
             src = (item as? ItemWithImage<*>)?.image,
             src = (item as? ItemWithImage<*>)?.image,
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
             contentScale = contentScale,
             contentScale = contentScale,
-            placeholder = placeholder
         )
         )
     },
     },
     cardWidth: Dp = computeCardWidth(),
     cardWidth: Dp = computeCardWidth(),
     focusState: MutableState<FocusState?> = rememberFocusState(item),
     focusState: MutableState<FocusState?> = rememberFocusState(item),
+    focusRequester: FocusRequester = rememberFocusRequester(item),
+    focused: Boolean = false,
     titlePadding: PaddingValues = PaddingValues(8.dp),
     titlePadding: PaddingValues = PaddingValues(8.dp),
     onFocus: (item: Any?) -> Unit = {},
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {},
@@ -69,6 +71,8 @@ fun ItemCard(
     modifier = modifier.recomposeHighlighter(),
     modifier = modifier.recomposeHighlighter(),
     item = item,
     item = item,
     focusState = focusState,
     focusState = focusState,
+    focusRequester = focusRequester,
+    focused = focused,
     contentScale = contentScale,
     contentScale = contentScale,
     aspectRatio = aspectRatio,
     aspectRatio = aspectRatio,
     textColor = textColor,
     textColor = textColor,
@@ -79,7 +83,6 @@ fun ItemCard(
     glow = glow,
     glow = glow,
     cardWidth = cardWidth,
     cardWidth = cardWidth,
     imageRenderer = imageRenderer,
     imageRenderer = imageRenderer,
-    placeholder = placeholder,
     titlePadding = titlePadding,
     titlePadding = titlePadding,
     onFocus = onFocus,
     onFocus = onFocus,
     onClick = onClick,
     onClick = onClick,

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

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.MutableState
+import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.focus.FocusState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.ContentScale
@@ -25,6 +26,7 @@ import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithDescription
@@ -41,6 +43,8 @@ fun PhotoCard(
     contentScale: ContentScale = ContentScale.Crop,
     contentScale: ContentScale = ContentScale.Crop,
     textColor: Color = Color.White,
     textColor: Color = Color.White,
     focusState: MutableState<FocusState?> = rememberFocusState(item),
     focusState: MutableState<FocusState?> = rememberFocusState(item),
+    focusRequester: FocusRequester = rememberFocusRequester(item),
+    focused: Boolean = false,
     @FloatRange(from = 0.0, to = 10.0)
     @FloatRange(from = 0.0, to = 10.0)
     contrast: Float = 5f,
     contrast: Float = 5f,
     @FloatRange(from = -255.0, to = 255.0)
     @FloatRange(from = -255.0, to = 255.0)
@@ -69,11 +73,12 @@ fun PhotoCard(
         contentScale = contentScale,
         contentScale = contentScale,
         textColor = textColor,
         textColor = textColor,
         focusState = focusState,
         focusState = focusState,
+        focusRequester = focusRequester,
+        focused = focused,
         aspectRatio = aspectRatio,
         aspectRatio = aspectRatio,
         cardWidth = cardWidth,
         cardWidth = cardWidth,
         scale = scale,
         scale = scale,
         imageRenderer = imageRenderer,
         imageRenderer = imageRenderer,
-        placeholder = placeholder,
         titlePadding = titlePadding,
         titlePadding = titlePadding,
         onFocus = onFocus,
         onFocus = onFocus,
         onClick = onClick,
         onClick = onClick,

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

@@ -41,7 +41,6 @@ fun CarouselCard(
     contentScale: ContentScale = ContentScale.Crop,
     contentScale: ContentScale = ContentScale.Crop,
     scale: CardScale = CardScale.None,
     scale: CardScale = CardScale.None,
     focusState: MutableState<FocusState?> = rememberFocusState(item),
     focusState: MutableState<FocusState?> = rememberFocusState(item),
-    placeholder: @Composable () -> Unit = {}, // todo
     imageRenderer: @Composable () -> Unit = {
     imageRenderer: @Composable () -> Unit = {
         val image = (item as? ItemWithImage<*>)?.image
         val image = (item as? ItemWithImage<*>)?.image
         val background = (item as? ItemWithBackground<*>)?.background
         val background = (item as? ItemWithBackground<*>)?.background
@@ -50,7 +49,6 @@ fun CarouselCard(
             src = background ?: image,
             src = background ?: image,
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
             contentScale = contentScale,
             contentScale = contentScale,
-            placeholder = placeholder
         )
         )
     },
     },
     titlePadding: PaddingValues = PaddingValues(8.dp, 12.dp, 8.dp, 12.dp),
     titlePadding: PaddingValues = PaddingValues(8.dp, 12.dp, 8.dp, 12.dp),
@@ -69,7 +67,6 @@ fun CarouselCard(
         contentScale = contentScale,
         contentScale = contentScale,
         scale = scale,
         scale = scale,
         focusState = focusState,
         focusState = focusState,
-        placeholder = placeholder,
         imageRenderer = imageRenderer,
         imageRenderer = imageRenderer,
         titlePadding = titlePadding,
         titlePadding = titlePadding,
         onFocus = onFocus,
         onFocus = onFocus,

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

@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusRequester
@@ -55,6 +56,7 @@ import org.mjdev.tvlib.ui.components.card.colorFocusBorder
 import org.mjdev.tvlib.ui.components.card.colorFocusGlow
 import org.mjdev.tvlib.ui.components.card.colorFocusGlow
 import org.mjdev.tvlib.ui.components.image.ImageAny
 import org.mjdev.tvlib.ui.components.image.ImageAny
 import org.mjdev.tvlib.ui.components.text.AutoHideEmptyText
 import org.mjdev.tvlib.ui.components.text.AutoHideEmptyText
+import timber.log.Timber
 
 
 @SuppressLint("ModifierParameter")
 @SuppressLint("ModifierParameter")
 @OptIn(ExperimentalTvMaterial3Api::class)
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -72,21 +74,20 @@ fun FocusableCard(
     colors: CardColors = CardDefaults.colors(),
     colors: CardColors = CardDefaults.colors(),
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
-    placeholder: @Composable () -> Unit = {},
     imageRenderer: @Composable () -> Unit = {
     imageRenderer: @Composable () -> Unit = {
         ImageAny(
         ImageAny(
             modifier = Modifier.fillMaxSize(),
             modifier = Modifier.fillMaxSize(),
             src = (item as? ItemWithImage<*>)?.image,
             src = (item as? ItemWithImage<*>)?.image,
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
             contentScale = contentScale,
             contentScale = contentScale,
-            placeholder = placeholder
         )
         )
     },
     },
     focusRequester: FocusRequester = rememberFocusRequester(item),
     focusRequester: FocusRequester = rememberFocusRequester(item),
     focusState: MutableState<FocusState?> = rememberFocusState(item),
     focusState: MutableState<FocusState?> = rememberFocusState(item),
+    focused: Boolean = false,
     onFocus: (item: Any?) -> Unit = {},
     onFocus: (item: Any?) -> Unit = {},
     onFocusChange: (state: FocusState) -> Unit = { state ->
     onFocusChange: (state: FocusState) -> Unit = { state ->
-        if (state.isFocused) {
+        if (state.isFocused || state.hasFocus) {
             onFocus(item)
             onFocus(item)
         }
         }
     },
     },
@@ -172,4 +173,13 @@ fun FocusableCard(
             onClick(item)
             onClick(item)
         },
         },
     )
     )
+    LaunchedEffect(focused) {
+        try {
+            if (focused) {
+                focusRequester.requestFocus()
+            }
+        } catch (e: Exception) {
+            Timber.e(e)
+        }
+    }
 }
 }

+ 217 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/Gallery.kt

@@ -0,0 +1,217 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.gallery
+
+import android.view.Gravity
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_DPAD_DOWN
+import android.view.KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableIntState
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableIntStateOf
+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.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyListState
+import androidx.tv.foundation.lazy.list.itemsIndexed
+import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import kotlinx.coroutines.delay
+import org.mjdev.tvlib.extensions.GlobalExt.toggle
+import org.mjdev.tvlib.interfaces.ItemPhoto
+import org.mjdev.tvlib.interfaces.ItemWithBackground
+import org.mjdev.tvlib.interfaces.ItemWithImage
+import org.mjdev.tvlib.ui.components.card.PhotoCard
+import org.mjdev.tvlib.ui.components.image.ImageAny
+import org.mjdev.tvlib.ui.components.immersivelist.ImmersiveList
+import org.mjdev.tvlib.ui.components.image.ImageBackground
+import org.mjdev.tvlib.ui.components.tv.TVRow
+import timber.log.Timber
+
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+@Composable
+fun Gallery(
+    modifier: Modifier = Modifier,
+    list: List<Any?> = listOf(),
+    index: Int = 0,
+    createColorBrush: (
+        color: Color,
+        gravity: Int
+    ) -> Brush = { color, g ->
+        Brush.verticalGradient(
+            when (g) {
+                Gravity.TOP -> listOf(
+                    color,
+                    color,
+                    color,
+                    color.copy(alpha = 0.8f),
+                    color.copy(alpha = 0.5f),
+                    Color.Transparent
+                )
+
+                else -> listOf(
+                    Color.Transparent,
+                    color.copy(alpha = 0.5f),
+                    color.copy(alpha = 0.8f),
+                    color,
+                    color,
+                    color,
+                )
+            }
+        )
+    },
+    infoVisible: MutableState<Boolean> = remember { mutableStateOf(true) },
+    currentItemIndex: MutableIntState = remember { mutableIntStateOf(index) },
+    focusRequester: FocusRequester = FocusRequester(),
+    imageFromItem: (index: Int) -> Any? = { idx ->
+        list[idx].let { data ->
+            val photo = (data as? ItemPhoto)?.uri
+            val image = (data as? ItemWithImage<*>)?.image
+            val background = (data as? ItemWithBackground<*>)?.background
+            photo ?: image ?: background
+        }
+    },
+    handleKey: (event: KeyEvent) -> Boolean = { ev ->
+        val action = ev.nativeKeyEvent.action
+        if (action == ACTION_DOWN) {
+            val code = ev.nativeKeyEvent.keyCode
+            if (infoVisible.value) {
+                infoVisible.value = true
+                if ((code == KEYCODE_DPAD_DOWN) || (code == KEYCODE_SYSTEM_NAVIGATION_DOWN)) {
+                    infoVisible.value = false
+                    true
+                } else {
+                    false
+                }
+            } else {
+                infoVisible.value = true
+                true
+            }
+        } else {
+            false
+        }
+    },
+    listState: TvLazyListState = rememberTvLazyListState(),
+) {
+    Box(
+        modifier = modifier.background(Color.Black, RectangleShape),
+        contentAlignment = Alignment.BottomCenter,
+    ) {
+        ImageBackground(
+            modifier = modifier,
+            image = imageFromItem(currentItemIndex.intValue),
+        )
+        ImmersiveList(
+            modifier = Modifier.fillMaxSize(),
+            currentItemIndex = currentItemIndex,
+            listAlignment = Alignment.BottomStart,
+            background = { _, _ ->
+                Box(
+                    modifier = Modifier.fillMaxSize()
+                ) {
+                    ImageAny(
+                        src = imageFromItem(currentItemIndex.intValue),
+                        contentDescription = null,
+                        contentScale = ContentScale.Fit,
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .focusable()
+                            .focusRequester(focusRequester)
+                            .pointerInput(Unit) {
+                                detectTapGestures {
+                                    infoVisible.toggle()
+                                }
+                            }
+                            .onKeyEvent { ev -> handleKey(ev) }
+                    )
+                    ItemInfo(
+                        src = list[currentItemIndex.intValue],
+                        infoVisible = infoVisible,
+                        createColorBrush = createColorBrush
+                    )
+                }
+            }
+        ) {
+            AnimatedVisibility(
+                visible = infoVisible.value,
+                enter = fadeIn() + slideInVertically(initialOffsetY = { it / 2 }),
+                exit = fadeOut() + slideOutVertically(targetOffsetY = { it / 2 }),
+            ) {
+                ImageBackground(
+                    modifier = Modifier.fillMaxWidth(),
+                    image = imageFromItem(currentItemIndex.intValue),
+                    transform = { color -> createColorBrush(color, Gravity.BOTTOM) }
+                ) {
+                    Spacer(modifier = Modifier.height(32.dp))
+                    TVRow(
+                        horizontalArrangement = Arrangement.spacedBy(16.dp),
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(16.dp),
+                        contentPadding = 16.dp,
+                        state = listState
+                    ) {
+                        itemsIndexed(list) { index, item ->
+                            PhotoCard(
+                                modifier = Modifier
+                                    .immersiveListItem(index)
+                                    .onKeyEvent { ev -> handleKey(ev) },
+                                item = item,
+                                focused = (currentItemIndex.intValue == index),
+                            )
+                        }
+                    }
+                    Spacer(modifier = Modifier.height(32.dp))
+                }
+                if (transition.currentState != transition.targetState) {
+                    if (!infoVisible.value) {
+                        try {
+                            focusRequester.requestFocus()
+                        } catch (e: Exception) {
+                            Timber.e(e)
+                        }
+                    }
+                }
+            }
+        }
+        LaunchedEffect(currentItemIndex.intValue) {
+            delay(5000)
+            infoVisible.value = false
+        }
+    }
+}

+ 127 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gallery/ItemInfo.kt

@@ -0,0 +1,127 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.gallery
+
+import android.view.Gravity
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.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.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.interfaces.ItemPhoto
+import org.mjdev.tvlib.interfaces.ItemWithBackground
+import org.mjdev.tvlib.interfaces.ItemWithDate
+import org.mjdev.tvlib.interfaces.ItemWithDescription
+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
+
+@TvPreview
+@Composable
+fun ItemInfo(
+    src: Any? = null,
+    infoVisible: MutableState<Boolean> = remember { mutableStateOf(true) },
+    createColorBrush: (
+        color: Color,
+        gravity: Int
+    ) -> Brush = { color, g ->
+        Brush.verticalGradient(
+            when (g) {
+                Gravity.TOP -> listOf(
+                    color,
+                    color,
+                    color,
+                    color.copy(alpha = 0.8f),
+                    color.copy(alpha = 0.5f),
+                    Color.Transparent
+                )
+
+                else -> listOf(
+                    Color.Transparent,
+                    color.copy(alpha = 0.5f),
+                    color.copy(alpha = 0.8f),
+                    color,
+                    color,
+                    color,
+                )
+            }
+        )
+    },
+    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
+    },
+    dateFromItem: () -> Any? = {
+        (src as? ItemWithDate)?.date
+    },
+    detailsFromItem: () -> Any? = {
+        (src as? ItemWithDescription<*>)?.description
+    },
+) {
+    AnimatedVisibility(
+        visible = infoVisible.value,
+        enter = fadeIn(),
+        exit = fadeOut(),
+    ) {
+        Box(
+            modifier = Modifier.fillMaxHeight(),
+            contentAlignment = Alignment.TopStart
+        ) {
+            ImageBackground(
+                modifier = Modifier.fillMaxWidth(),
+                image = imageFromItem(),
+                transform = { color -> createColorBrush(color, Gravity.TOP) }
+            ) {
+                Column(
+                    modifier = Modifier.padding(16.dp, 0.dp, 0.dp, 16.dp)
+                ) {
+                    Spacer(modifier = Modifier.height(16.dp))
+                    AutoHideEmptyText(
+                        text = titleFromItem(),
+                        color = Color.White,
+                        fontSize = 24.sp
+                    )
+                    AutoHideEmptyText(
+                        text = dateFromItem(),
+                        color = Color.White,
+                        fontSize = 16.sp
+                    )
+                    AutoHideEmptyText(
+                        text = detailsFromItem(),
+                        color = Color.White,
+                        fontSize = 12.sp
+                    )
+                    Spacer(modifier = Modifier.height(16.dp))
+                }
+            }
+        }
+    }
+}

+ 2 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/FadingPhotoImage.kt

@@ -56,7 +56,8 @@ fun FadingPhotoImage(
     val fadingImage = remember { mutableStateOf(initialImage) }
     val fadingImage = remember { mutableStateOf(initialImage) }
     Crossfade(
     Crossfade(
         fadingImage.value,
         fadingImage.value,
-        animationSpec = tween(animationDuration)
+        animationSpec = tween(animationDuration),
+        label = "FadingImage"
     ) { targetState ->
     ) { targetState ->
         PhotoImage(
         PhotoImage(
             modifier = modifier.blur(radius = blurRadius),
             modifier = modifier.blur(radius = blurRadius),

+ 30 - 10
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/ImageAny.kt

@@ -36,19 +36,17 @@ import org.mjdev.tvlib.extensions.DrawableExt.asImageBitmap
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import java.net.URL
 import java.net.URL
 
 
-// todo placeholder
 @SuppressLint("ModifierParameter")
 @SuppressLint("ModifierParameter")
 @Preview
 @Preview
 @Composable
 @Composable
 fun ImageAny(
 fun ImageAny(
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
-    src: Any? = R.drawable.person,
+    src: Any? = null,
     contentDescription: String? = null,
     contentDescription: String? = null,
     alignment: Alignment = Alignment.Center,
     alignment: Alignment = Alignment.Center,
     contentScale: ContentScale = ContentScale.Fit,
     contentScale: ContentScale = ContentScale.Fit,
     alpha: Float = DefaultAlpha,
     alpha: Float = DefaultAlpha,
     colorFilter: ColorFilter? = null,
     colorFilter: ColorFilter? = null,
-    placeholder: @Composable () -> Unit = {}, // todo on error
 ) = BoxWithConstraints(
 ) = BoxWithConstraints(
     modifier = modifier
     modifier = modifier
 ) {
 ) {
@@ -59,7 +57,17 @@ fun ImageAny(
 
 
     when (src) {
     when (src) {
 
 
-        null, Unit -> placeholder()
+        null, Unit -> {
+            Image(
+                painterResource(R.drawable.broken_image),
+                contentDescription,
+                modifier.recomposeHighlighter(),
+                alignment,
+                contentScale,
+                alpha,
+                colorFilter
+            )
+        }
 
 
         Color -> Image(
         Color -> Image(
             ColorDrawable((src as Color).toArgb()).asImageBitmap(width, height),
             ColorDrawable((src as Color).toArgb()).asImageBitmap(width, height),
@@ -122,9 +130,13 @@ fun ImageAny(
         )
         )
 
 
         is URL -> CoilImage(
         is URL -> CoilImage(
-            imageLoader = { imageLoader },
+            imageLoader = {
+                imageLoader
+            },
             modifier = modifier.recomposeHighlighter(),
             modifier = modifier.recomposeHighlighter(),
-            imageModel = { src },
+            imageModel = {
+                src
+            },
             imageOptions = ImageOptions(
             imageOptions = ImageOptions(
                 contentScale = contentScale,
                 contentScale = contentScale,
                 alignment = alignment,
                 alignment = alignment,
@@ -135,9 +147,13 @@ fun ImageAny(
         )
         )
 
 
         is Uri -> CoilImage(
         is Uri -> CoilImage(
-            imageLoader = { imageLoader },
+            imageLoader = {
+                imageLoader
+            },
             modifier = modifier.recomposeHighlighter(),
             modifier = modifier.recomposeHighlighter(),
-            imageModel = { src },
+            imageModel = {
+                src
+            },
             imageOptions = ImageOptions(
             imageOptions = ImageOptions(
                 contentScale = contentScale,
                 contentScale = contentScale,
                 alignment = alignment,
                 alignment = alignment,
@@ -148,9 +164,13 @@ fun ImageAny(
         )
         )
 
 
         is String -> CoilImage(
         is String -> CoilImage(
-            imageLoader = { imageLoader },
+            imageLoader = {
+                imageLoader
+            },
             modifier = modifier.recomposeHighlighter(),
             modifier = modifier.recomposeHighlighter(),
-            imageModel = { src },
+            imageModel = {
+                src
+            },
             imageOptions = ImageOptions(
             imageOptions = ImageOptions(
                 contentScale = contentScale,
                 contentScale = contentScale,
                 alignment = alignment,
                 alignment = alignment,

+ 57 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/ImageBackground.kt

@@ -0,0 +1,57 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.image
+
+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.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+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.platform.LocalContext
+import androidx.core.graphics.drawable.toBitmap
+import coil.request.ImageRequest
+import org.mjdev.tvlib.extensions.BitmapExt.majorColor
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
+import org.mjdev.tvlib.extensions.ModifierExt.conditional
+
+@Composable
+fun ImageBackground(
+    modifier: Modifier = Modifier,
+    image: Any? = null,
+    transform: ((color: Color) -> Brush)? = null,
+    contentAlignment: Alignment = Alignment.TopStart,
+    content: @Composable () -> Unit = {}
+) {
+    val context = LocalContext.current
+    val imageLoader = rememberImageLoader()
+    val bckColor = remember { mutableStateOf(Color.Transparent) }
+    Box(
+        modifier = modifier
+            .conditional(transform == null) {
+                background(bckColor.value, RectangleShape)
+            }
+            .conditional(transform != null) {
+                background(transform!!.invoke(bckColor.value), RectangleShape)
+            },
+        contentAlignment = contentAlignment
+    ) {
+        content()
+    }
+    LaunchedEffect(image) {
+        bckColor.value = imageLoader.execute(
+            ImageRequest.Builder(context).data(image).build()
+        ).drawable?.toBitmap()?.majorColor(Color.Black) ?: Color.Black
+    }
+}

+ 28 - 13
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/PhotoImage.kt

@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
 import androidx.core.graphics.drawable.toDrawable
 import androidx.core.graphics.drawable.toDrawable
 import com.skydoves.landscapist.ImageOptions
 import com.skydoves.landscapist.ImageOptions
 import com.skydoves.landscapist.coil.CoilImage
 import com.skydoves.landscapist.coil.CoilImage
+import com.skydoves.landscapist.coil.CoilImageState
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.extensions.BitmapExt.majorColor
 import org.mjdev.tvlib.extensions.BitmapExt.majorColor
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
@@ -41,14 +42,20 @@ import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.DrawableExt.asPhoto
 import org.mjdev.tvlib.extensions.DrawableExt.asPhoto
 import org.mjdev.tvlib.extensions.ModifierExt.conditional
 import org.mjdev.tvlib.extensions.ModifierExt.conditional
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
+import timber.log.Timber
 
 
 @SuppressLint("ModifierParameter")
 @SuppressLint("ModifierParameter")
 @Preview
 @Preview
 @Composable
 @Composable
 fun PhotoImage(
 fun PhotoImage(
-    src: Any? = R.drawable.person,
-    placeholder: @Composable () -> Unit = {},
+    src: Any? = null,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
+    placeholder: @Composable () -> Unit = {
+        ImageAny(
+            modifier = modifier,
+            src = R.drawable.broken_image
+        )
+    },
     alignment: Alignment = Alignment.Center,
     alignment: Alignment = Alignment.Center,
     contentScale: ContentScale = ContentScale.Crop,
     contentScale: ContentScale = ContentScale.Crop,
     alpha: Float = DefaultAlpha,
     alpha: Float = DefaultAlpha,
@@ -63,13 +70,25 @@ fun PhotoImage(
     shape: Shape = RoundedCornerShape(roundCornerSize),
     shape: Shape = RoundedCornerShape(roundCornerSize),
     colorFilter: ColorFilter? = null,
     colorFilter: ColorFilter? = null,
     contentDescription: String? = null,
     contentDescription: String? = null,
+    onImageStateChanged: (state: CoilImageState) -> Unit = {}
 ) {
 ) {
     val isEdit = isEditMode()
     val isEdit = isEditMode()
     val context = LocalContext.current
     val context = LocalContext.current
     val imageLoader = rememberImageLoader()
     val imageLoader = rememberImageLoader()
+    val imageOptions = ImageOptions(
+        contentScale = contentScale,
+        alignment = alignment,
+        contentDescription = contentDescription,
+        alpha = alpha,
+        colorFilter = colorFilter,
+    )
     CoilImage(
     CoilImage(
-        imageLoader = { imageLoader },
-        imageModel = { src },
+        imageLoader = {
+            imageLoader
+        },
+        imageModel = {
+            src
+        },
         modifier = modifier
         modifier = modifier
             .recomposeHighlighter()
             .recomposeHighlighter()
             .conditional(isEdit) {
             .conditional(isEdit) {
@@ -79,17 +98,13 @@ fun PhotoImage(
                 BorderStroke(borderSize, borderColor),
                 BorderStroke(borderSize, borderColor),
                 shape
                 shape
             ),
             ),
-        imageOptions = ImageOptions(
-            contentScale = contentScale,
-            alignment = alignment,
-            contentDescription = contentDescription,
-            alpha = alpha,
-            colorFilter = colorFilter,
-        ),
-        failure = {
+        imageOptions = imageOptions,
+        failure = { failure ->
+            Timber.e(failure.reason)
             placeholder()
             placeholder()
         },
         },
-        onImageStateChanged = {
+        onImageStateChanged = { state ->
+            onImageStateChanged.invoke(state)
         },
         },
         success = { state, _ ->
         success = { state, _ ->
             val bitmap = state.imageBitmap?.asAndroidBitmap()
             val bitmap = state.imageBitmap?.asAndroidBitmap()

+ 62 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/immersivelist/ImmersiveList.kt

@@ -0,0 +1,62 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.immersivelist
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableIntState
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+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.onFocusChanged
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import org.mjdev.tvlib.extensions.ModifierExt.bringIntoViewIfChildrenAreFocused
+
+@Suppress("IllegalExperimentalApiUsage")
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalTvMaterial3Api
+@Composable
+fun ImmersiveList(
+    background: @Composable ImmersiveListBackgroundScope.(index: Int, listHasFocus: Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+    listAlignment: Alignment = Alignment.BottomEnd,
+    currentItemIndex: MutableIntState = remember { mutableIntStateOf(0) },
+    listHasFocus: MutableState<Boolean> = remember { mutableStateOf(false) },
+    list: @Composable ImmersiveListScope.() -> Unit,
+) {
+    Box(
+        modifier.bringIntoViewIfChildrenAreFocused()
+    ) {
+        ImmersiveListBackgroundScope(
+            this
+        ).background(
+            currentItemIndex.intValue,
+            listHasFocus.value
+        )
+        val focusManager = LocalFocusManager.current
+        Box(
+            Modifier
+                .align(listAlignment)
+                .onFocusChanged { focusState ->
+                    listHasFocus.value = focusState.hasFocus
+                }
+        ) {
+            ImmersiveListScope { idx ->
+                currentItemIndex.intValue = idx
+                focusManager.moveFocus(FocusDirection.Enter)
+            }.list()
+        }
+    }
+}

+ 70 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/immersivelist/ImmersiveListBackgroundScope.kt

@@ -0,0 +1,70 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.immersivelist
+
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+import androidx.tv.material3.ImmersiveListDefaults
+
+@Immutable
+@ExperimentalTvMaterial3Api
+class ImmersiveListBackgroundScope internal constructor(boxScope: BoxScope) : BoxScope by boxScope {
+
+    @Suppress("unused")
+    @Composable
+    fun AnimatedVisibility(
+        visible: Boolean,
+        modifier: Modifier = Modifier,
+        enter: EnterTransition = ImmersiveListDefaults.EnterTransition,
+        exit: ExitTransition = ImmersiveListDefaults.ExitTransition,
+        label: String = "AnimatedVisibility",
+        content: @Composable AnimatedVisibilityScope.() -> Unit
+    ) {
+        androidx.compose.animation.AnimatedVisibility(
+            visible,
+            modifier,
+            enter,
+            exit,
+            label,
+            content
+        )
+    }
+
+    @Suppress("unused")
+    @Composable
+    fun AnimatedContent(
+        targetState: Int,
+        modifier: Modifier = Modifier,
+        transitionSpec: AnimatedContentTransitionScope<Int>.() -> ContentTransform = {
+            ImmersiveListDefaults.EnterTransition.togetherWith(ImmersiveListDefaults.ExitTransition)
+        },
+        contentAlignment: Alignment = Alignment.TopStart,
+        label: String = "AnimatedContent",
+        content: @Composable AnimatedVisibilityScope.(targetState: Int) -> Unit
+    ) {
+        androidx.compose.animation.AnimatedContent(
+            targetState,
+            modifier,
+            transitionSpec,
+            contentAlignment,
+            content = content,
+            label = label
+        )
+    }
+}

+ 30 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/immersivelist/ImmersiveListScope.kt

@@ -0,0 +1,30 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2023.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.tvlib.ui.components.immersivelist
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.tv.material3.ExperimentalTvMaterial3Api
+
+@Immutable
+@ExperimentalTvMaterial3Api
+class ImmersiveListScope internal constructor(
+    private val onFocused: (Int) -> Unit
+) {
+
+    fun Modifier.immersiveListItem(index: Int): Modifier {
+        return this then onFocusChanged {
+            if (it.isFocused) {
+                onFocused(index)
+            }
+        }
+    }
+
+}

+ 3 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/text/AutoHideEmptyText.kt

@@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.TextUnit
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.LocalTextStyle
 import androidx.tv.material3.LocalTextStyle
+import org.mjdev.tvlib.extensions.ComposeExt.textFrom
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -48,7 +49,8 @@ fun AutoHideEmptyText(
     onTextLayout: (TextLayoutResult) -> Unit = {},
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
     style: TextStyle = LocalTextStyle.current
 ) {
 ) {
-    if ((text != null) && (text.toString().isNotEmpty())) {
+    val textAny = textFrom(text)
+    if (textAny.isNotEmpty()) {
         TextAny(
         TextAny(
             text,
             text,
             modifier.recomposeHighlighter(),
             modifier.recomposeHighlighter(),

+ 22 - 16
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/ErrorMessage.kt

@@ -37,6 +37,7 @@ import org.mjdev.tvlib.ui.components.button.Button
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.ui.components.text.TextAny
 import org.mjdev.tvlib.ui.components.text.TextAny
 
 
+@Suppress("LocalVariableName")
 @OptIn(ExperimentalTvMaterial3Api::class)
 @OptIn(ExperimentalTvMaterial3Api::class)
 @TvPreview
 @TvPreview
 @Composable
 @Composable
@@ -58,11 +59,14 @@ fun ErrorMessage(
 ) {
 ) {
 
 
     Box(
     Box(
-        modifier = Modifier.fillMaxWidth().recomposeHighlighter()
+        modifier = Modifier
+            .fillMaxWidth()
+            .recomposeHighlighter()
     ) {
     ) {
 
 
         ConstraintLayout(
         ConstraintLayout(
-            modifier = Modifier.recomposeHighlighter()
+            modifier = Modifier
+                .recomposeHighlighter()
                 .fillMaxWidth()
                 .fillMaxWidth()
                 .padding(contentPadding)
                 .padding(contentPadding)
                 .background(backgroundColor, RoundedCornerShape(roundSize))
                 .background(backgroundColor, RoundedCornerShape(roundSize))
@@ -79,7 +83,8 @@ fun ErrorMessage(
             }
             }
 
 
             TextAny(
             TextAny(
-                modifier = Modifier.recomposeHighlighter()
+                modifier = Modifier
+                    .recomposeHighlighter()
                     .wrapContentSize()
                     .wrapContentSize()
                     .constrainAs(_title) {
                     .constrainAs(_title) {
                         top.linkTo(parent.top)
                         top.linkTo(parent.top)
@@ -94,7 +99,8 @@ fun ErrorMessage(
             )
             )
 
 
             BasicText(
             BasicText(
-                modifier = Modifier.recomposeHighlighter()
+                modifier = Modifier
+                    .recomposeHighlighter()
                     .wrapContentSize()
                     .wrapContentSize()
                     .constrainAs(_message) {
                     .constrainAs(_message) {
                         top.linkTo(parent.top)
                         top.linkTo(parent.top)
@@ -113,36 +119,36 @@ fun ErrorMessage(
                 maxLines = 4,
                 maxLines = 4,
                 text =
                 text =
                 error?.message ?: (stringResource(id = R.string.error_unknown) +
                 error?.message ?: (stringResource(id = R.string.error_unknown) +
-                    "\nType: " +
-                    (error?.javaClass?.simpleName ?: Exception::class.simpleName))
+                        "\nType: " +
+                        (error?.javaClass?.simpleName ?: Exception::class.simpleName))
             )
             )
 
 
             if (dismissible && (cancelText != null)) {
             if (dismissible && (cancelText != null)) {
 
 
                 Button(
                 Button(
-                    modifier = Modifier.recomposeHighlighter()
+                    modifier = Modifier
+                        .recomposeHighlighter()
                         .padding(4.dp)
                         .padding(4.dp)
                         .constrainAs(_button) {
                         .constrainAs(_button) {
                             top.linkTo(parent.top, margin = 4.dp)
                             top.linkTo(parent.top, margin = 4.dp)
                             end.linkTo(parent.end, margin = roundSize)
                             end.linkTo(parent.end, margin = roundSize)
                             bottom.linkTo(parent.bottom, margin = 4.dp)
                             bottom.linkTo(parent.bottom, margin = 4.dp)
                         },
                         },
-                    contentPadding=PaddingValues(
+                    contentPadding = PaddingValues(
                         8.dp,
                         8.dp,
                         4.dp
                         4.dp
                     ),
                     ),
                     containerColor = Color.Red,
                     containerColor = Color.Red,
                     onClick = {
                     onClick = {
                         onDismiss()
                         onDismiss()
+                    },
+                    content = {
+                        TextAny(
+                            text = cancelText,
+                            color = color
+                        )
                     }
                     }
-                ) {
-
-                    TextAny(
-                        text = cancelText,
-                        color = color
-                    )
-
-                }
+                )
 
 
             }
             }
 
 

+ 0 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/TVRow.kt

@@ -24,7 +24,6 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 
 
-// todo : scroll horizontally by touch
 @TvPreview
 @TvPreview
 @Composable
 @Composable
 fun TVRow(
 fun TVRow(

+ 18 - 0
tvlib/src/main/res/drawable/broken_image.xml

@@ -0,0 +1,18 @@
+<!--
+  ~  Copyright (c) Milan Jurkulák 2023.
+  ~  Contact:
+  ~  e: mimoccc@gmail.com
+  ~  e: mj@mjdev.org
+  ~  w: https://mjdev.org
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/black"
+      android:pathData="M180,840Q156,840 138,822Q120,804 120,780L120,180Q120,156 138,138Q156,120 180,120L780,120Q804,120 822,138Q840,156 840,180L840,780Q840,804 822,822Q804,840 780,840L180,840ZM223,526L395,354L565,524L736,353L780,397L780,180Q780,180 780,180Q780,180 780,180L180,180Q180,180 180,180Q180,180 180,180L180,483L223,526ZM180,780L780,780Q780,780 780,780Q780,780 780,780L780,482L736,438L565,609L395,439L223,611L180,568L180,780Q180,780 180,780Q180,780 180,780ZM180,780L180,780Q180,780 180,780Q180,780 180,780L180,482L180,542L180,457.1L180,397L180,180Q180,180 180,180Q180,180 180,180L180,180Q180,180 180,180Q180,180 180,180L180,483L180,483L180,568L180,568L180,780Q180,780 180,780Q180,780 180,780Z"/>
+</vector>

BIN
tvlib/src/main/res/drawable/person.png


+ 12 - 0
tvlib/src/main/res/drawable/person.xml

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/black"
+      android:pathData="M222,705Q285,661 347,637.5Q409,614 480,614Q551,614 613.5,637.5Q676,661 739,705Q783,651 801.5,596Q820,541 820,480Q820,335 722.5,237.5Q625,140 480,140Q335,140 237.5,237.5Q140,335 140,480Q140,541 159,596Q178,651 222,705ZM479.81,510Q422,510 382.5,470.31Q343,430.63 343,372.81Q343,315 382.69,275.5Q422.37,236 480.19,236Q538,236 577.5,275.69Q617,315.37 617,373.19Q617,431 577.31,470.5Q537.63,510 479.81,510ZM480.47,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635.23Q80,562.47 80,479.73Q80,397 111.5,324.5Q143,252 197.5,197.5Q252,143 324.77,111.5Q397.53,80 480.27,80Q563,80 635.5,111.5Q708,143 762.5,197.5Q817,252 848.5,324.53Q880,397.06 880,479.53Q880,562 848.5,635Q817,708 762.5,762.5Q708,817 635.47,848.5Q562.94,880 480.47,880ZM480,820Q535,820 587.5,804Q640,788 691,748Q640,712 587,693Q534,674 480,674Q426,674 373,693Q320,712 269,748Q320,788 372.5,804Q425,820 480,820ZM480,450Q514,450 535.5,428.5Q557,407 557,373Q557,339 535.5,317.5Q514,296 480,296Q446,296 424.5,317.5Q403,339 403,373Q403,407 424.5,428.5Q446,450 480,450ZM480,373Q480,373 480,373Q480,373 480,373Q480,373 480,373Q480,373 480,373Q480,373 480,373Q480,373 480,373Q480,373 480,373Q480,373 480,373ZM480,747Q480,747 480,747Q480,747 480,747Q480,747 480,747Q480,747 480,747Q480,747 480,747Q480,747 480,747Q480,747 480,747Q480,747 480,747Z"
+      tools:ignore="VectorPath" />
+</vector>