Parcourir la source

image loader improvements

Milan Jurkulak il y a 3 ans
Parent
commit
a8548df3af
38 fichiers modifiés avec 272 ajouts et 101 suppressions
  1. 5 1
      app/build.gradle.kts
  2. 3 1
      app/src/main/AndroidManifest.xml
  3. 1 0
      app/src/main/kotlin/org/mjdev/tvapp/data/local/Movie.kt
  4. 2 1
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/AboutPage.kt
  5. 8 9
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  6. 8 10
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt
  7. 29 0
      tvlib/build.gradle.kts
  8. 12 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt
  9. 9 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt
  10. 1 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/GlobalExt.kt
  11. 8 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ModifierExt.kt
  12. 11 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavExt.kt
  13. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/ItemCard.kt
  14. 7 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/card/PhotoCard.kt
  15. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/BigCarousel.kt
  16. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/CarouselCard.kt
  17. 2 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gridview/TvGridView.kt
  18. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/CircleImage.kt
  19. 5 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/FadingPhotoImage.kt
  20. 2 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/ImageAny.kt
  21. 9 9
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/PhotoImage.kt
  22. 2 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt
  23. 7 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavHostEx.kt
  24. 5 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/Navigation.kt
  25. 3 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt
  26. 12 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/Page.kt
  27. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/screen/ScreenView.kt
  28. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/AppsRow.kt
  29. 9 2
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/BrowseView.kt
  30. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CategoryRow.kt
  31. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CursorRow.kt
  32. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Header.kt
  33. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalAudioRow.kt
  34. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalPhotosRow.kt
  35. 6 1
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalVideoRow.kt
  36. 3 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Tabs.kt
  37. 5 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/UserPic.kt
  38. 39 46
      tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt

+ 5 - 1
app/build.gradle.kts

@@ -202,7 +202,7 @@ dependencies {
     implementation(project(mapOf("path" to ":tvlib")))
     // base libs
     implementation("androidx.core:core-ktx:1.12.0")
-    implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
     // compose base libs
     implementation(platform("androidx.compose:compose-bom:2023.09.01"))
     implementation("androidx.compose.ui:ui-tooling")
@@ -227,6 +227,7 @@ dependencies {
     implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11")
     // debug
     implementation("com.jakewharton.timber:timber:5.0.1")
+    implementation("com.android.volley:volley:1.2.1")
     // kapt
     kapt("com.google.dagger:dagger-compiler:2.48")
     kapt("com.google.dagger:dagger-android-processor:2.48")
@@ -279,6 +280,9 @@ dependencies {
     implementation("androidx.media3:media3-decoder:1.1.1")
     implementation("androidx.media3:media3-datasource:1.1.1")
     implementation("androidx.media3:media3-common:1.1.1")
+    // images
+    implementation("io.coil-kt:coil-base:2.4.0")
+    implementation("com.github.skydoves:landscapist-coil:2.2.9")
     // libs mismatch
     constraints {
         implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0").apply {

+ 3 - 1
app/src/main/AndroidManifest.xml

@@ -172,7 +172,8 @@
 
         <service
             android:name=".sync.SyncService"
-            android:exported="false">
+            android:process=":sync"
+            android:exported="true">
 
             <intent-filter>
                 <action android:name="android.content.SyncAdapter" />
@@ -204,6 +205,7 @@
             android:enabled="true"
             android:exported="true"
             android:syncable="true"
+            android:process=":provider"
             tools:ignore="ExportedContentProvider" />
 
         <receiver

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

@@ -63,6 +63,7 @@ class Movie :
     @Json(name = "category")
     var category: String? = ""
         get() = (field ?: country)?.replaceFirstChar { ch ->
+            // todo : and
             ch.titlecase(Locale.ROOT)
         }
 

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

@@ -67,7 +67,8 @@ class AboutPage : Page() {
                         CircleImage(
                             modifier = Modifier.size(128.dp),
                             contentPadding = 10.dp,
-                            src = R.drawable.milanj
+                            src = R.drawable.milanj,
+                            imageLoader = imageLoader
                         )
                         CardContent(
                             modifier = Modifier.fillMaxWidth(),

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

@@ -96,14 +96,6 @@ class MainPage : Page() {
         val authManager = rememberAuthManager { u -> user.value = u }
         val apps = remember { viewModel.apps }.collectAsState(emptyList())
 
-//        val systemUIController = rememberSystemUIController {
-//            if (isLandscape)  {
-//                hideSystemUI()
-//            } else {
-//                showSystemUI()
-//            }
-//        }
-
         Box(
             modifier = Modifier.fillMaxSize(),
         ) {
@@ -124,6 +116,7 @@ class MainPage : Page() {
                 },
                 onItemFocused = onItemSelect,
                 onItemClicked = onItemClick,
+                imageLoader = imageLoader,
                 onUserPicClicked = {
                     if (authManager.isUserLoggedIn) {
                         navController?.openSettings()
@@ -133,13 +126,17 @@ class MainPage : Page() {
                 },
                 customRows = mutableListOf<@Composable () -> Unit>().apply {
                     add {
-                        AppsRow(apps = apps)
+                        AppsRow(
+                            apps = apps,
+                            imageLoader = imageLoader
+                        )
                     }
                     if (viewModel.localAudioCursor.count > 0) add {
                         LocalAudioRow(
                             cursor = viewModel.localAudioCursor,
                             openItem = { item -> onItemClick(item) },
                             onItemFocus = onItemSelect,
+                            imageLoader = imageLoader
                         )
                     }
                     if (viewModel.localVideoCursor.count > 0) add {
@@ -147,6 +144,7 @@ class MainPage : Page() {
                             cursor = viewModel.localVideoCursor,
                             openItem = { item -> onItemClick(item) },
                             onItemFocus = onItemSelect,
+                            imageLoader = imageLoader
                         )
                     }
                     if (viewModel.localPhotoCursor.count > 0) add {
@@ -154,6 +152,7 @@ class MainPage : Page() {
                             cursor = viewModel.localPhotoCursor,
                             openItem = { item -> onItemClick(item) },
                             onItemFocus = onItemSelect,
+                            imageLoader = imageLoader
                         )
                     }
                 }

+ 8 - 10
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt

@@ -81,18 +81,16 @@ class MainViewModel @Inject constructor(
         }
     }.stateInViewModel()
 
+    // todo anr
     val movieList: StateFlow<Map<String, List<Movie>>> = flow {
-        dao.movieDao.all.map { movie ->
-            movie.apply {
-                category = category ?: noCategoryString
+        dao.movieDao.all
+            .sortedBy { m ->
+                m.category
+            }.asMap { m ->
+                Pair(m.category, m)
+            }.also { map ->
+                emit(map)
             }
-        }.sortedBy { m ->
-            m.category
-        }.asMap { m ->
-            Pair(m.category, m)
-        }.also { map ->
-            emit(map)
-        }
     }.stateInViewModel()
 
     val countryList: StateFlow<List<String>> = flow {

+ 29 - 0
tvlib/build.gradle.kts

@@ -35,12 +35,41 @@ android {
     }
 
     buildTypes {
+        debug {
+            isMinifyEnabled = false
+            proguardFiles(
+                getDefaultProguardFile("proguard-android-optimize.txt"),
+                "proguard-rules.pro"
+            )
+            buildConfigField(
+                "Boolean",
+                "RECOMPOSE_ENABLED",
+                "false"
+            )
+        }
         release {
             isMinifyEnabled = false
             proguardFiles(
                 getDefaultProguardFile("proguard-android-optimize.txt"),
                 "proguard-rules.pro"
             )
+            buildConfigField(
+                "Boolean",
+                "RECOMPOSE_ENABLED",
+                "false"
+            )
+        }
+        create("recomposing") {
+            isMinifyEnabled = false
+            proguardFiles(
+                getDefaultProguardFile("proguard-android-optimize.txt"),
+                "proguard-rules.pro"
+            )
+            buildConfigField(
+                "Boolean",
+                "RECOMPOSE_ENABLED",
+                "true"
+            )
         }
     }
 

+ 12 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt

@@ -30,12 +30,14 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
 import androidx.tv.material3.NonInteractiveSurfaceDefaults
 import androidx.tv.material3.Surface
+import coil.ImageLoader
 import com.github.anrwatchdog.ANRWatchDog
 import org.mjdev.tvlib.BuildConfig
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ContextExt.isATv
 import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
-import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
+import org.mjdev.tvlib.extensions.NavExt.navControllerEx
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
 import org.mjdev.tvlib.helpers.other.ConfigChangeCallback
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
@@ -62,7 +64,9 @@ open class ComposableActivity : ComponentActivity() {
     open val roundCornerSize: Dp = 0.dp
     open val backgroundShape: Shape = RoundedCornerShape(roundCornerSize)
 
-    lateinit var navController: NavHostControllerEx
+    val navController: NavHostControllerEx by lazy {
+        navControllerEx(this)
+    }
 
     var lastIntent: Intent? = null
 
@@ -74,12 +78,15 @@ open class ComposableActivity : ComponentActivity() {
             }
     }
 
+    val imageLoader: ImageLoader
+        @Composable
+        get() = rememberImageLoader()
+
     @Previews
     @OptIn(ExperimentalTvMaterial3Api::class)
     @Composable
     @CallSuper
     open fun Compose() {
-        navController = rememberNavControllerEx()
         MaterialTheme {
             Surface(
                 modifier = Modifier
@@ -103,7 +110,8 @@ open class ComposableActivity : ComponentActivity() {
                 NavHostEx(
                     modifier = Modifier.fillMaxSize(),
                     navController = navController,
-                    builder = navGraphBuilder
+                    builder = navGraphBuilder,
+                    imageLoader = imageLoader
                 )
             }
         }

+ 9 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt

@@ -13,6 +13,9 @@ package org.mjdev.tvlib.extensions
 import android.annotation.SuppressLint
 import android.content.ContentResolver
 import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
 import android.os.Build
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.runtime.Composable
@@ -30,6 +33,7 @@ import androidx.compose.ui.platform.LocalInspectionMode
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toDrawable
 import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
@@ -170,6 +174,11 @@ object ComposeExt {
         }
     }
 
+    @Composable
+    fun Bitmap.toDrawable(
+        resources: Resources = LocalContext.current.resources
+    ): BitmapDrawable = BitmapDrawable(resources, this)
+
     @SuppressLint("RememberReturnType", "ComposableNaming")
     @Composable
     fun rememberImageLoader(): ImageLoader {

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

@@ -62,7 +62,7 @@ object GlobalExt {
     }
 
     @Suppress("SENSELESS_COMPARISON")
-    val String.isUrl:Boolean
+    val String.isUrl: Boolean
         get() = try {
             URL(this) != null
         } catch (e: MalformedURLException) {

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

@@ -43,10 +43,12 @@ import androidx.compose.ui.graphics.lerp
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.delay
 import org.mjdev.tvlib.BuildConfig
+import org.mjdev.tvlib.R
 import org.mjdev.tvlib.extensions.ComposeExt.isLandscapeMode
 import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
 import timber.log.Timber
@@ -127,10 +129,15 @@ object ModifierExt {
         )
     }
 
+
+    val isRecomposeHighlighterEnabled: Boolean
+        @Composable
+        get() = BuildConfig.RECOMPOSE_ENABLED
+
     @Composable
     @Stable
     fun Modifier.recomposeHighlighter(
-        isComposeDebug: Boolean = BuildConfig.DEBUG
+        isComposeDebug: Boolean = isRecomposeHighlighterEnabled
     ): Modifier = conditional(
         isComposeDebug
     ) {

+ 11 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/NavExt.kt

@@ -58,6 +58,17 @@ object NavExt {
         }
     }
 
+    fun navControllerEx(
+        context:Context,
+        vararg navigators: Navigator<out NavDestination>
+    ): NavHostControllerEx {
+        return createNavController(context).apply {
+            for (navigator in navigators) {
+                navigatorProvider.addNavigator(navigator)
+            }
+        }
+    }
+
     private fun createNavController(context: Context) =
         NavHostControllerEx(context).apply {
             navigatorProvider.addNavigator(ComposeNavigator())

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

@@ -27,11 +27,14 @@ import androidx.tv.material3.CardGlow
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.CardShape
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
@@ -53,12 +56,14 @@ fun ItemCard(
     border: CardBorder = CardDefaults.colorFocusBorder(Color.Green),
     glow: CardGlow = CardDefaults.colorFocusGlow(Color.Green),
     aspectRatio: Float = 16f / 9f,
+    imageLoader: ImageLoader = rememberImageLoader(),
     imageRenderer: @Composable () -> Unit = {
         ImageAny(
             modifier = modifier.fillMaxSize(),
             src = (item as? ItemWithImage<*>)?.image,
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
             contentScale = contentScale,
+            imageLoader = imageLoader
         )
     },
     cardWidth: Dp = computeCardWidth(),

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

@@ -24,12 +24,15 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.material3.CardDefaults
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ComposeExt.isFocused
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithDescription
 import org.mjdev.tvlib.interfaces.ItemWithImage
@@ -44,7 +47,7 @@ fun PhotoCard(
     modifier: Modifier = Modifier,
     contentScale: ContentScale = ContentScale.Crop,
     textColor: Color = Color.White,
-    focused: Boolean = ComposeExt.isEditMode(),
+    focused: Boolean = isEditMode(),
     focusState: MutableState<FocusState?> = rememberFocusState(
         item,
         FocusHelper(focused)
@@ -54,6 +57,7 @@ fun PhotoCard(
     contrast: Float = 5f,
     @FloatRange(from = -255.0, to = 255.0)
     brightness: Float = -255f,
+    imageLoader: ImageLoader = rememberImageLoader(),
     imageRenderer: @Composable () -> Unit = {
         PhotoImage(
             modifier = Modifier.fillMaxSize(),
@@ -61,6 +65,7 @@ fun PhotoCard(
             contentScale = contentScale,
             contrast = contrast,
             brightness = brightness,
+            imageLoader = imageLoader,
             borderColor = if (focusState.isFocused) Color.Green else Color.Black,
             contentDescription = (item as? ItemWithDescription<*>)?.description?.toString(),
         )
@@ -76,6 +81,7 @@ fun PhotoCard(
 ) {
     ItemCard(
         item = item,
+        imageLoader = imageLoader,
         modifier = modifier.recomposeHighlighter(),
         contentScale = contentScale,
         textColor = textColor,

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/carousel/BigCarousel.kt

@@ -23,9 +23,12 @@ import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.unit.dp
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.detectSwipe
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.ui.components.card.FocusHelper
@@ -38,6 +41,7 @@ fun BigCarousel(
     modifier: Modifier = Modifier,
     items: List<Any?> = listOf(Unit, Unit, Unit),
     autoScrollDurationMillis: Long = 8000,
+    imageLoader: ImageLoader = rememberImageLoader(),
     carouselState: CarouselState = remember(items) { CarouselState() },
     onItemSelected: ((movie: Any?) -> Unit)? = null,
     onItemClicked: ((movie: Any?) -> Unit)? = null,
@@ -85,6 +89,7 @@ fun BigCarousel(
             contentScale = ContentScale.Crop,
             scale = CardScale.None,
             focusState = cardFocusState,
+            imageLoader = imageLoader,
             onFocus = {
                 onItemSelected?.invoke(it)
             },

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

@@ -20,9 +20,12 @@ import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.dp
 import androidx.tv.material3.CardScale
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.conditional
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithBackground
@@ -56,6 +59,7 @@ fun CarouselCard(
             contentScale = contentScale,
         )
     },
+    imageLoader: ImageLoader = rememberImageLoader(),
     titlePadding: PaddingValues = PaddingValues(8.dp, 12.dp, 8.dp, 12.dp),
     onFocus: (item: Any?) -> Unit = {},
     onClick: (item: Any?) -> Unit = {}
@@ -76,6 +80,7 @@ fun CarouselCard(
         imageRenderer = imageRenderer,
         titlePadding = titlePadding,
         onFocus = onFocus,
+        imageLoader = imageLoader,
         onClick = { onClick(item) }
     )
 }

+ 2 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/gridview/TvGridView.kt

@@ -25,6 +25,7 @@ import androidx.tv.foundation.lazy.grid.rememberTvLazyGridState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt
+import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
 import org.mjdev.tvlib.ui.components.card.PhotoCard
 
@@ -42,7 +43,7 @@ fun TvGridView(
         Unit, Unit, Unit
     ),
     state: TvLazyGridState = rememberTvLazyGridState(),
-    cardWidth: Dp = ComposeExt.computeCardWidth(),
+    cardWidth: Dp = computeCardWidth(),
     aspectRatio: Float = 16f / 9f,
     contentScale: ContentScale = ContentScale.Crop,
     onItemFocus: (item: Any?) -> Unit = {},

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/image/CircleImage.kt

@@ -24,8 +24,11 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import coil.ImageLoader
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 
 @SuppressLint("ModifierParameter")
@@ -38,7 +41,8 @@ fun CircleImage(
     borderSize: Dp = 3.dp,
     contentDescription: String = "",
     contentPadding: Dp = 2.dp,
-    src: Any? = R.drawable.person
+    src: Any? = R.drawable.person,
+    imageLoader: ImageLoader = rememberImageLoader(),
 ) {
     Box(
         modifier = modifier
@@ -62,6 +66,7 @@ fun CircleImage(
             src = src,
             contentDescription = contentDescription,
             contentScale = ContentScale.Crop,
+            imageLoader = imageLoader
         )
     }
 }

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

@@ -26,8 +26,10 @@ import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import coil.ImageLoader
 import kotlinx.coroutines.delay
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 
 @Previews
 @Composable
@@ -52,6 +54,7 @@ fun FadingPhotoImage(
     colorFilter: ColorFilter? = null,
     contentDescription: String? = null,
     alignment: Alignment = Alignment.Center,
+    imageLoader: ImageLoader = rememberImageLoader(),
 ) {
     val fadingImage = remember { mutableStateOf(initialImage) }
     Crossfade(
@@ -72,7 +75,8 @@ fun FadingPhotoImage(
             alpha = alpha,
             colorFilter = colorFilter,
             contentDescription = contentDescription,
-            alignment = alignment
+            alignment = alignment,
+            imageLoader = imageLoader
         )
     }
     LaunchedEffect(fadingImageState.value) {

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

@@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
+import coil.ImageLoader
 import com.skydoves.landscapist.ImageOptions
 import com.skydoves.landscapist.coil.CoilImage
 import org.mjdev.tvlib.R
@@ -49,12 +50,12 @@ fun ImageAny(
     contentScale: ContentScale = ContentScale.Fit,
     alpha: Float = DefaultAlpha,
     colorFilter: ColorFilter? = null,
+    imageLoader: ImageLoader = rememberImageLoader(),
 ) = BoxWithConstraints(
     modifier = modifier,
     contentAlignment = Alignment.Center
 ) {
 
-    val imageLoader = rememberImageLoader()
     val width = if (constraints.minWidth == 0) 1 else constraints.minWidth
     val height = if (constraints.minHeight == 0) 1 else constraints.minHeight
 

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

@@ -29,7 +29,7 @@ import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.core.graphics.drawable.toDrawable
+import coil.ImageLoader
 import com.skydoves.landscapist.ImageOptions
 import com.skydoves.landscapist.coil.CoilImage
 import com.skydoves.landscapist.coil.CoilImageState
@@ -39,6 +39,7 @@ import org.mjdev.tvlib.extensions.BitmapExt.majorColor
 import org.mjdev.tvlib.extensions.ColorExt.contrastAndBrightness
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
+import org.mjdev.tvlib.extensions.ComposeExt.toDrawable
 import org.mjdev.tvlib.extensions.DrawableExt.asPhoto
 import org.mjdev.tvlib.extensions.ModifierExt.conditional
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
@@ -70,18 +71,17 @@ fun PhotoImage(
     shape: Shape = RoundedCornerShape(roundCornerSize),
     colorFilter: ColorFilter? = null,
     contentDescription: String? = null,
-    onImageStateChanged: (state: CoilImageState) -> Unit = {}
-) {
-    val isEdit = isEditMode()
-    val context = LocalContext.current
-    val imageLoader = rememberImageLoader()
-    val imageOptions = ImageOptions(
+    imageLoader: ImageLoader = rememberImageLoader(),
+    imageOptions: ImageOptions = ImageOptions(
         contentScale = contentScale,
         alignment = alignment,
         contentDescription = contentDescription,
         alpha = alpha,
         colorFilter = colorFilter,
-    )
+    ),
+    onImageStateChanged: (state: CoilImageState) -> Unit = {}
+) {
+    val isEdit = isEditMode()
     CoilImage(
         imageLoader = {
             imageLoader
@@ -116,7 +116,7 @@ fun PhotoImage(
             ) {
                 ImageAny(
                     src = bitmap
-                        ?.toDrawable(context.resources)
+                        ?.toDrawable()
                         ?.asPhoto(),
                     modifier = modifier.recomposeHighlighter(),
                     alignment = alignment,

+ 2 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/NativePlayerImpl.kt

@@ -38,6 +38,7 @@ import androidx.media3.common.VideoSize
 import androidx.media3.common.text.CueGroup
 import androidx.media3.common.util.Size
 import org.mjdev.tvlib.extensions.ComposeExt
+import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 
 // todo
@@ -54,7 +55,7 @@ class NativePlayerImpl(
     ) {
         val width = LocalConfiguration.current.screenWidthDp
         val height = LocalConfiguration.current.screenHeightDp
-        val isEdit = ComposeExt.isEditMode()
+        val isEdit = isEditMode()
         Box(
             modifier = Modifier
                 .recomposeHighlighter()

+ 7 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/NavHostEx.kt

@@ -22,7 +22,9 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.ContentScale
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.compose.NavHost
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.NavExt.createGraph
 import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
@@ -40,6 +42,7 @@ fun NavHostEx(
     route: String? = null,
     startRoute: String? = null,
     navController: NavHostControllerEx = rememberNavControllerEx(),
+    imageLoader: ImageLoader = rememberImageLoader(),
     contentAlignment: Alignment = Alignment.Center,
     enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = {
         fadeIn(animationSpec = tween(700))
@@ -61,11 +64,13 @@ fun NavHostEx(
         contrast = 4f,
         alpha = 0.7f,
         brightness = -255f,
-        contentScale = ContentScale.Crop
+        contentScale = ContentScale.Crop,
+        imageLoader = imageLoader
     )
     ScreenView(
         modifier = modifier.recomposeHighlighter(),
         navController = navController,
+//        imageLoader = imageLoader
     ) {
         NavHost(
             modifier = modifier.recomposeHighlighter(),
@@ -83,6 +88,7 @@ fun NavHostEx(
             exitTransition = exitTransition,
             popEnterTransition = popEnterTransition,
             popExitTransition = popExitTransition,
+//            imageLoader = imageLoader
         )
     }
 }

+ 5 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/Navigation.kt

@@ -35,6 +35,7 @@ fun Navigation(
     modifier: Modifier = Modifier,
     navController: NavHostControllerEx = rememberNavControllerEx(),
     content: @Composable () -> Unit = { Page() },
+//    imageLoader: ImageLoader = rememberImageLoader(),
     menuItems: List<MenuItem> = if (isEditMode()) listOf(
         MenuItem.MENU_ITEM_EXIT,
         MenuItem.MENU_ITEM_SETTINGS,
@@ -64,7 +65,8 @@ fun Navigation(
             drawerState = navController.settingsDrawerState,
             onTouchOutside = {
                 navController.closeSettings()
-            }
+            },
+//            imageLoader = imageLoader
         ) {
             if (navController.isMenuEnabled) {
                 val drawerContent: @Composable (DrawerValue) -> Unit = { state ->
@@ -94,7 +96,8 @@ fun Navigation(
                         modifier = modifier.fillMaxHeight(),
                         drawerState = navController.menuDrawerState,
                         content = mainContent,
-                        drawerContent = drawerContent
+                        drawerContent = drawerContent,
+//                        imageLoader = imageLoader
                     )
                 }
             } else {

+ 3 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/navigation/SettingsDrawer.kt

@@ -17,7 +17,9 @@ import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.rememberDrawerState
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -25,6 +27,7 @@ import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 @Composable
 fun SettingsDrawer(
     modifier: Modifier = Modifier,
+//    imageLoader: ImageLoader = ComposeExt.rememberImageLoader(),
     drawerState: DrawerState = rememberDrawerState(
         if (isEditMode()) DrawerValue.Open
         else DrawerValue.Closed

+ 12 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/page/Page.kt

@@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.sp
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusRequester
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.ModifierExt.requestFocusOnTouch
 import org.mjdev.tvlib.ui.components.complex.VerticalScrollableBox
@@ -37,12 +38,19 @@ open class Page {
     open val title: Any? = null
     open val icon: Any? = null
 
+    val focusRequester
+        @Composable
+        get() = rememberFocusRequester(title)
+
+    val imageLoader
+        @Composable
+        get() = rememberImageLoader()
+
     @SuppressLint("ComposableNaming")
     @Previews
     @Composable
     @CallSuper
     fun content() {
-        val focusRequester = rememberFocusRequester(title)
         Column(
             Modifier
                 .fillMaxSize()
@@ -79,7 +87,9 @@ open class Page {
             horizontalAlignment = Alignment.CenterHorizontally,
         ) {
             TextAny(
-                modifier = Modifier.fillMaxSize().recomposeHighlighter(),
+                modifier = Modifier
+                    .fillMaxSize()
+                    .recomposeHighlighter(),
                 text = title ?: "Empty Page",
                 textAlign = TextAlign.Center,
                 fontSize = 32.sp,

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

@@ -13,7 +13,10 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.extensions.NavExt.rememberNavControllerEx
 import org.mjdev.tvlib.navigation.NavHostControllerEx
@@ -26,6 +29,7 @@ import org.mjdev.tvlib.ui.components.page.Page
 fun ScreenView(
     modifier: Modifier = Modifier,
     navController: NavHostControllerEx = rememberNavControllerEx(),
+//    imageLoader: ImageLoader = rememberImageLoader(),
     content: @Composable () -> Unit = { Page() }
 ) {
     Box(
@@ -37,7 +41,8 @@ fun ScreenView(
         if (navController.isMenuEnabled) {
             Navigation(
                 navController = navController,
-                content = content
+                content = content,
+//                imageLoader = imageLoader
             )
         } else content()
     }

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/AppsRow.kt

@@ -22,11 +22,14 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.ImageLoader
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
 import org.mjdev.tvlib.helpers.apps.App
 import org.mjdev.tvlib.helpers.apps.rememberAppsManager
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.ui.components.card.ItemCard
 
 @OptIn(ExperimentalTvMaterial3Api::class)
@@ -42,6 +45,7 @@ fun AppsRow(
     startApp: Context.(app: App?) -> Unit = { app ->
         startActivity(app?.intent)
     },
+    imageLoader: ImageLoader = rememberImageLoader(),
     apps: State<List<App>> = rememberAppsManager().collectAsState(emptyList())
 ) {
     val context: Context = LocalContext.current
@@ -68,6 +72,7 @@ fun AppsRow(
                     onClick = { app ->
                         startApp(context, app as? App)
                     },
+                    imageLoader = imageLoader
                 )
             },
         )

+ 9 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/BrowseView.kt

@@ -19,11 +19,14 @@ import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import coil.ImageLoader
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.ui.components.carousel.BigCarousel
 
 @Previews
@@ -50,6 +53,7 @@ fun BrowseView(
     verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(32.dp),
     contentPadding: PaddingValues = PaddingValues(8.dp),
     customRows: List<@Composable () -> Unit> = emptyList(),
+    imageLoader: ImageLoader = rememberImageLoader(),
     onTitleClicked: () -> Unit = {},
     onClockClicked: () -> Unit = {},
     onMessageBadgeClicked: () -> Unit = {},
@@ -75,6 +79,7 @@ fun BrowseView(
                 onClockClick = onClockClicked,
                 onMessageBadgeClick = onMessageBadgeClicked,
                 onUserPicClick = onUserPicClicked,
+                imageLoader = imageLoader,
             )
         }
         item {
@@ -120,7 +125,8 @@ fun BrowseView(
                 modifier = Modifier.recomposeHighlighter(),
                 items = featuredItems,
                 onItemSelected = onItemFocused,
-                onItemClicked = onItemClicked
+                onItemClicked = onItemClicked,
+                imageLoader = imageLoader
             )
         }
         items(customRows) { row -> row.invoke() }
@@ -133,7 +139,8 @@ fun BrowseView(
                 onItemFocus = {
                     onItemFocused(it)
                 },
-                onItemClick = onItemClicked
+                onItemClick = onItemClicked,
+                imageLoader = imageLoader,
             )
         }
     }

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CategoryRow.kt

@@ -31,8 +31,11 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.ui.components.card.PhotoCard
 import org.mjdev.tvlib.ui.components.text.TextAny
@@ -53,13 +56,15 @@ fun CategoryRow(
     backgroundShape: Shape = RoundedCornerShape(roundCornerSize),
     onItemFocus: (item: Any?) -> Unit = {},
     onItemClick: (item: Any?) -> Unit = {},
+    imageLoader: ImageLoader = rememberImageLoader(),
     contentOfItem: @Composable (item: Any?) -> Unit = { item ->
         PhotoCard(
             item = item,
             contentScale = contentScale,
             cardWidth = cardWidth,
             onClick = onItemClick,
-            onFocus = onItemFocus
+            onFocus = onItemFocus,
+            imageLoader = imageLoader,
         )
     },
 ) {

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/CursorRow.kt

@@ -36,8 +36,11 @@ import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import androidx.tv.material3.ExperimentalTvMaterial3Api
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.extensions.CursorExt.getData
 import org.mjdev.tvlib.extensions.CursorExt.isNotEmpty
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
@@ -64,6 +67,7 @@ fun CursorRow(
     selection: String? = null,
     selectionArgs: Array<String>? = null,
     sortOrder: String? = null,
+    imageLoader: ImageLoader = rememberImageLoader(),
     transform: (Cursor) -> Any? = { it },
     cursor: Cursor? = rememberCursor(
         uri = uri,
@@ -119,6 +123,7 @@ fun CursorRow(
                         onClick = { i ->
                             openItem(context, i)
                         },
+                        imageLoader = imageLoader
                     )
                 }
             }

+ 4 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Header.kt

@@ -30,11 +30,13 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import coil.ImageLoader
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
 import org.mjdev.tvlib.extensions.ComposeExt.isPortraitMode
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.ui.components.badge.Badge
 import org.mjdev.tvlib.ui.components.complex.FocusableBox
 
@@ -52,6 +54,7 @@ fun Header(
     padding: Dp = 0.dp,
     contentPadding: Dp = 2.dp,
     messagesCount: Int = 0,
+    imageLoader: ImageLoader = rememberImageLoader(),
     onTitleClick: () -> Unit = {},
     onClockClick: () -> Unit = {},
     onMessageBadgeClick: () -> Unit = {},
@@ -111,6 +114,7 @@ fun Header(
                 contentPadding = contentPadding,
                 borderSize = 1.dp,
                 onClick = onUserPicClick,
+                imageLoader = imageLoader
             )
         }
         Row(

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalAudioRow.kt

@@ -19,9 +19,12 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+import coil.ImageLoader
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.helpers.cursor.AudioItem
 import org.mjdev.tvlib.helpers.cursor.CachingCursor.Companion.rememberCursor
 
@@ -40,6 +43,7 @@ fun LocalAudioRow(
     sortOrder: String? = AudioItem.SORT_ORDER_DATE_DESC,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
+    imageLoader: ImageLoader = rememberImageLoader(),
     transform: (Cursor) -> Any? = { c -> AudioItem(c) },
     cursor: Cursor? = rememberCursor(
         uri = AudioItem.URI,
@@ -68,5 +72,6 @@ fun LocalAudioRow(
     transform = transform,
     onItemFocus = onItemFocus,
     openItem = openItem,
-    cursor = cursor
+    cursor = cursor,
+    imageLoader = imageLoader
 )

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalPhotosRow.kt

@@ -19,9 +19,12 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+import coil.ImageLoader
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.helpers.cursor.CachingCursor.Companion.rememberCursor
 import org.mjdev.tvlib.helpers.cursor.PhotoItem
 
@@ -40,6 +43,7 @@ fun LocalPhotosRow(
     sortOrder: String? = PhotoItem.SORT_ORDER_DATE_DESC,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
+    imageLoader: ImageLoader = rememberImageLoader(),
     transform: (Cursor) -> Any? = { c -> PhotoItem(c) },
     cursor: Cursor? = rememberCursor(
         uri = PhotoItem.URI,
@@ -68,5 +72,6 @@ fun LocalPhotosRow(
     transform = transform,
     openItem = openItem,
     onItemFocus = onItemFocus,
-    cursor = cursor
+    cursor = cursor,
+    imageLoader = imageLoader
 )

+ 6 - 1
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalVideoRow.kt

@@ -19,9 +19,12 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
+import coil.ImageLoader
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.helpers.cursor.CachingCursor.Companion.rememberCursor
 import org.mjdev.tvlib.helpers.cursor.VideoItem
 
@@ -40,6 +43,7 @@ fun LocalVideoRow(
     sortOrder: String? = VideoItem.SORT_ORDER_DATE_DESC,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
+    imageLoader: ImageLoader = rememberImageLoader(),
     transform: (Cursor) -> Any? = { c -> VideoItem(c) },
     cursor: Cursor? = rememberCursor(
         uri = VideoItem.URI,
@@ -69,6 +73,7 @@ fun LocalVideoRow(
         transform = { c -> VideoItem(c) },
         openItem = openItem,
         onItemFocus = onItemFocus,
-        cursor = cursor
+        cursor = cursor,
+        imageLoader = imageLoader
     )
 }

+ 3 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/Tabs.kt

@@ -29,7 +29,9 @@ import androidx.tv.material3.Tab
 import androidx.tv.material3.TabDefaults
 import androidx.tv.material3.TabRow
 import androidx.tv.material3.TabRowDefaults
+import coil.ImageLoader
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
 import org.mjdev.tvlib.extensions.ComposeExt.isFocused
 import org.mjdev.tvlib.extensions.ComposeExt.rememberFocusState
 import org.mjdev.tvlib.extensions.ComposeExt.rememberMutableInteractionSource
@@ -50,6 +52,7 @@ fun Tabs(
     activeContentColor: Color = Color.Green,
     selectedContentColor: Color = Color.White,
     focusedContentColor: Color = Color.White,
+//    imageLoader: ImageLoader = rememberImageLoader(),
     interactionSource: MutableInteractionSource = rememberMutableInteractionSource(items),
     onItemClick: (item: Any?) -> Unit = {}
 ) {

+ 5 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/UserPic.kt

@@ -19,11 +19,14 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import androidx.tv.material3.MaterialTheme
+import coil.ImageLoader
 import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.conditional
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.Previews
+import org.mjdev.tvlib.extensions.ComposeExt
+import org.mjdev.tvlib.extensions.ComposeExt.rememberImageLoader
 import org.mjdev.tvlib.ui.components.complex.FocusableBox
 import org.mjdev.tvlib.ui.components.image.CircleImage
 
@@ -38,6 +41,7 @@ fun UserPic(
     borderColor: Color = Color.White,
     borderSize: Dp = 2.dp,
     contentPadding: Dp = 2.dp,
+    imageLoader: ImageLoader = rememberImageLoader(),
     onClick: () -> Unit = {}
 ) = FocusableBox(
     modifier = modifier.wrapContentSize(),
@@ -55,5 +59,6 @@ fun UserPic(
         borderColor = borderColor,
         contentPadding = contentPadding + borderSize,
         src = src,
+        imageLoader = imageLoader
     )
 }

+ 39 - 46
tvlib/src/main/kotlin/org/mjdev/tvlib/updater/AppUpdater.kt

@@ -20,9 +20,10 @@ import androidx.compose.ui.platform.LocalContext
 import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
 import com.skydoves.sandwich.adapters.ApiResponseCallAdapterFactory
 import com.skydoves.sandwich.getOrNull
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.runBlocking
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
@@ -52,17 +53,22 @@ class AppUpdater(
         )
     }
 
-    private val lastReleaseTimestamp: Long get() = releases.filter { release ->
-        release.hasAPK
-    }.maxByOrNull { release ->
-        release.publishedAt.asTimestamp
-    }?.publishedAt?.asTimestamp ?: 0L
+    private val releases: Flow<List<Release>> = flow {
+        emit(service.releases().getOrNull() ?: emptyList())
+    }
 
-    private val installedTimestamp = prefs.getLong(AppUpdater::class.simpleName, 0L)
+    private val lastRelease
+        get() = releases.map { list ->
+            list.filter { it.hasAPK }.maxOfOrNull { it.publishedAt }
+        }
 
-//    private val httpClient by lazy {
-//        okHttpClient()
-//    }
+    private val serverTimestamp: Long
+        get() = runBlocking {
+            lastRelease.firstOrNull()?.asTimestamp ?: 0L
+        }
+
+    private val installedTimestamp
+        get() = prefs.getLong(AppUpdater::class.simpleName, serverTimestamp)
 
 //    private val externalFile: File
 //        get() = File(
@@ -70,44 +76,31 @@ class AppUpdater(
 //            "$githubRepository-update.apk"
 //        )
 
-    private val releases: List<Release>
-        get() = runBlocking {
-            service.releases().getOrNull() ?: emptyList()
-        }
-
-    val isUpdateAvailable: Boolean get() = checkUpdate()
-
-    private fun checkUpdate(): Boolean {
-        return if (installedTimestamp == 0L) {
-            prefs.edit()
-                .putLong(AppUpdater::class.simpleName, lastReleaseTimestamp)
-                .apply()
-            false
-        } else {
-            true
-        }
-    }
+    val isUpdateAvailable: Boolean get() = serverTimestamp > installedTimestamp
 
     @MainThread
     fun updateApp() {
-        CoroutineScope(Dispatchers.IO).launch {
-            releases.filter { release ->
-                release.hasAPK
-            }.maxByOrNull { release ->
-                release.publishedAt.asTimestamp
-            }?.let { release ->
-                release.apkAsset?.downloadUrl
-            }?.also { url ->
-                downloadFile(url) {
-                    prefs.edit()
-                        .putLong(AppUpdater::class.simpleName, lastReleaseTimestamp)
-                        .apply()
-//                    CoroutineScope(Dispatchers.Main).launch {
-//                        startUpdate()
+        // todo
+//        CoroutineScope(Dispatchers.IO).launch {
+//            releases.collectLatest { rs ->
+//                rs.filter { r ->
+//                    r.hasAPK
+//                }.maxByOrNull { r ->
+//                    r.publishedAt.asTimestamp
+//                }?.let { release ->
+//                    release.apkAsset?.downloadUrl
+//                }?.also { url ->
+//                    downloadFile(url) {
+//                        prefs.edit()
+//                            .putLong(AppUpdater::class.simpleName, lastReleaseTimestamp)
+//                            .apply()
+////                        CoroutineScope(Dispatchers.Main).launch {
+////                            startUpdate()
+////                        }
 //                    }
-                }
-            }
-        }
+//                }
+//            }
+//        }
     }
 
     @AnyThread