Sfoglia il codice sorgente

recompose fixes

recompose fixes #2

recompose fixes #4
Milan Jurkulak 3 anni fa
parent
commit
5f2dcd0663

+ 4 - 2
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/ComposeExt.kt

@@ -94,9 +94,11 @@ object ComposeExt {
 
     @SuppressLint("RememberReturnType", "ComposableNaming")
     @Composable
-    fun rememberContentResolver() : ContentResolver {
+    fun rememberContentResolver(
+        key: Any? = Unit
+    ): ContentResolver {
         val context = LocalContext.current
-        return remember {
+        return remember(key) {
             context.contentResolver
         }
     }

+ 12 - 21
app/src/main/kotlin/org/mjdev/tvapp/base/extensions/CursorExt.kt

@@ -13,33 +13,24 @@ import androidx.core.database.getBlobOrNull
 import androidx.core.database.getFloatOrNull
 import androidx.core.database.getIntOrNull
 import androidx.core.database.getStringOrNull
-import org.mjdev.tvapp.base.helpers.cursor.CursorItem
 import timber.log.Timber
 
 object CursorExt {
 
-    fun Cursor.toList(
+    val Cursor.isNotEmpty get() = count > 0
+
+    fun Cursor.asMap(
         projection: Array<String>
-    ): List<CursorItem> {
-        val c: Cursor = this
-        return ArrayList<CursorItem>().also { list ->
-            if (c.count > 0) {
-                c.moveToFirst()
-                do {
-                    list.add(CursorItem().also { ci ->
-                        projection.forEach { cname ->
-                            val idx = c.getColumnIndex(cname)
-                            val value = try {
-                                c.get(idx)
-                            } catch (e: Exception) {
-                                Timber.e(e)
-                                null
-                            }
-                            ci[cname] = value
-                        }
-                    })
-                } while (moveToNext())
+    ): Map<String, Any?> = mutableMapOf<String, Any?>().apply {
+        projection.forEach { cname ->
+            val idx = getColumnIndex(cname)
+            val value = try {
+                this@asMap.get(idx)
+            } catch (e: Exception) {
+                Timber.e(e)
+                null
             }
+            this[cname] = value
         }
     }
 

+ 22 - 33
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/apps/AppsManager.kt

@@ -10,54 +10,43 @@ package org.mjdev.tvapp.base.helpers.apps
 
 import android.content.Context
 import android.content.Intent
-import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
 import android.graphics.drawable.Drawable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.interfaces.ItemWithIntent
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
 import java.util.Collections
 
+// todo saveable, parcelable is too big
 @Composable
-fun rememberAppsManager(): Flow<List<App>> {
+fun rememberAppsManager(): List<App> {
     val context: Context = LocalContext.current
     return remember {
-        flow {
-            val packageManager = context.packageManager
-            val mainIntent = Intent(Intent.ACTION_MAIN, null).apply {
-                addCategory(Intent.CATEGORY_LAUNCHER)
-            }
-            val resolveInfo = packageManager.queryIntentActivities(mainIntent, 0)
-            val apps = mutableListOf<App>()
-            Collections.sort(resolveInfo, ResolveInfo.DisplayNameComparator(packageManager))
+        val packageManager = context.packageManager
+        val mainIntent = Intent(Intent.ACTION_MAIN, null).apply {
+            addCategory(Intent.CATEGORY_LAUNCHER)
+        }
+        val resolveInfo = packageManager.queryIntentActivities(mainIntent, 0)
+        Collections.sort(resolveInfo, ResolveInfo.DisplayNameComparator(packageManager))
+        mutableListOf<App>().apply {
             resolveInfo.forEach { ri ->
-                apps.add(App(ri, packageManager))
-                emit(apps)
+                val title = ri.activityInfo.loadLabel(packageManager).toString()
+                val image = ri.activityInfo
+                    .loadBanner(packageManager) ?: ri.activityInfo
+                    .loadIcon(packageManager)
+                val intent: Intent? =
+                    packageManager.getLaunchIntentForPackage(ri.activityInfo.packageName)
+                add(App(title, image, intent))
             }
-        }.flowOn(Dispatchers.IO)
+        }
     }
 }
 
-class App(
-    ri: ResolveInfo,
-    packageManager: PackageManager,
-) : ItemWithTitle<String>, ItemWithImage<Drawable>, ItemWithIntent {
-
-    override var title: String? = ri.activityInfo
-        .loadLabel(packageManager).toString()
-
-    override var image: Drawable? = ri.activityInfo
-        .loadBanner(packageManager) ?: ri.activityInfo
-        .loadIcon(packageManager)
-
-    override var intent: Intent? = packageManager
-        .getLaunchIntentForPackage(ri.activityInfo.packageName)
-
-}
+data class App(
+    override var title: String?,
+    override var image: Drawable?,
+    override var intent: Intent?,
+) : ItemWithTitle<String>, ItemWithImage<Drawable>, ItemWithIntent

+ 5 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/AudioItem.kt

@@ -9,10 +9,12 @@
 package org.mjdev.tvapp.base.helpers.cursor
 
 import android.content.ContentResolver
+import android.database.Cursor
 import android.net.Uri
 import android.os.Build
 import android.provider.MediaStore
 import android.util.Size
+import org.mjdev.tvapp.base.extensions.CursorExt.asMap
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
 import org.mjdev.tvapp.base.interfaces.ItemWithUri
@@ -20,11 +22,11 @@ import timber.log.Timber
 
 class AudioItem(
     private val contentResolver: ContentResolver,
-    ci: CursorItem
-) : CursorItem(), ItemWithTitle<String>, ItemWithImage<Any>, ItemWithUri<Uri> {
+    c: Cursor
+) : HashMap<String, Any?>(), ItemWithTitle<String>, ItemWithImage<Any>, ItemWithUri<Uri> {
 
     init {
-        putAll(ci)
+        putAll(c.asMap(MEDIA_PROJECTION))
     }
 
     override val title: String?

+ 58 - 39
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/Cursor.kt

@@ -11,57 +11,76 @@
 package org.mjdev.tvapp.base.helpers.cursor
 
 import android.annotation.SuppressLint
+import android.content.ContentResolver
 import android.content.Context
-import android.database.ContentObserver
+import android.database.Cursor
 import android.net.Uri
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.flowOn
-import org.mjdev.tvapp.base.extensions.CursorExt.toList
-import timber.log.Timber
+import org.mjdev.tvapp.base.extensions.CursorExt.isNotEmpty
 
+@SuppressLint("Recycle")
 @Composable
-fun rememberMediaCursor(
+fun rememberCursor(
     uri: Uri?,
     projection: Array<String>,
     selection: String? = null,
     selectionArgs: Array<String>? = null,
     sortOrder: String? = null,
-): Flow<List<CursorItem>> {
-    val context: Context = LocalContext.current
-    return remember {
-        callbackFlow {
-            val resolver = context.contentResolver
-            val observer = object : ContentObserver(null) {
-                @SuppressLint("Recycle")
-                override fun onChange(selfChange: Boolean) {
-                    if (uri != null) {
-                        try {
-                            resolver.query(
-                                uri, projection, selection, selectionArgs, sortOrder
-                            )?.toList(projection)?.let { data ->
-                                trySend(data)
-                            }
-                        } catch (e: Exception) {
-                            Timber.e(e)
-                        }
-                    } else {
-                        trySend(emptyList())
-                    }
-                }
+    prefetchItems: Int = 8,
+    transform: (Cursor) -> Any?,
+): PrefetchCursor? {
+    return if (uri == null) null
+    else PrefetchCursor(
+        LocalContext.current,
+        uri,
+        projection,
+        selection,
+        selectionArgs,
+        sortOrder,
+        prefetchItems,
+        transform,
+    )
+}
+
+// todo prefetching & cache
+@Suppress("UNUSED_PARAMETER")
+class PrefetchCursor(
+    context: Context,
+    uri: Uri?,
+    private val projection: Array<String>,
+    selection: String? = null,
+    selectionArgs: Array<String>? = null,
+    sortOrder: String? = null,
+    prefetchItems: Int = 8,
+    private val transform: (Cursor) -> Any? = { it },
+) {
+
+    private val resolver: ContentResolver by lazy {
+        context.contentResolver
+    }
+    private val cursor: Cursor? by lazy {
+        if (uri != null) {
+            resolver.query(
+                uri, projection, selection, selectionArgs, sortOrder
+            ).apply {
+                if (this?.isNotEmpty == true) moveToFirst()
             }
-            if (uri != null) {
-                resolver.registerContentObserver(uri, true, observer)
-                observer.onChange(true)
-                awaitClose {
-                    resolver.unregisterContentObserver(observer)
-                }
+        } else null
+    }
+
+    val count: Int get() = cursor?.count ?: 0
+    val isNotEmpty: Boolean get() = count > 0
+
+    fun get(idx: Int): Any? {
+        var result: Any? = null
+        if (count > idx) {
+            cursor?.move(idx)
+            result = cursor?.let { c ->
+                transform(c)
             }
-        }.flowOn(Dispatchers.IO)
+        }
+        return result
     }
+
 }

+ 0 - 16
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/CursorItem.kt

@@ -1,16 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2023.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.tvapp.base.helpers.cursor
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-import java.io.Serializable
-
-@Parcelize
-open class CursorItem : HashMap<String, Any?>(), Serializable, Parcelable

+ 5 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/PhotoItem.kt

@@ -8,17 +8,19 @@
 
 package org.mjdev.tvapp.base.helpers.cursor
 
+import android.database.Cursor
 import android.net.Uri
 import android.provider.MediaStore
+import org.mjdev.tvapp.base.extensions.CursorExt.asMap
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
 
 class PhotoItem(
-    ci: CursorItem
-) : CursorItem(), ItemWithTitle<String>, ItemWithImage<Any> {
+    c: Cursor
+) : HashMap<String, Any?>(), ItemWithTitle<String>, ItemWithImage<Any> {
 
     init {
-        putAll(ci)
+        putAll(c.asMap(MEDIA_PROJECTION))
     }
 
     override val title: String?

+ 5 - 3
app/src/main/kotlin/org/mjdev/tvapp/base/helpers/cursor/VideoItem.kt

@@ -9,10 +9,12 @@
 package org.mjdev.tvapp.base.helpers.cursor
 
 import android.content.ContentResolver
+import android.database.Cursor
 import android.net.Uri
 import android.os.Build
 import android.provider.MediaStore
 import android.util.Size
+import org.mjdev.tvapp.base.extensions.CursorExt.asMap
 import org.mjdev.tvapp.base.interfaces.ItemWithImage
 import org.mjdev.tvapp.base.interfaces.ItemWithTitle
 import org.mjdev.tvapp.base.interfaces.ItemWithUri
@@ -20,8 +22,8 @@ import timber.log.Timber
 
 class VideoItem(
     private val contentResolver: ContentResolver,
-    ci: CursorItem
-) : CursorItem(), ItemWithTitle<String>, ItemWithImage<Any>, ItemWithUri<Uri> {
+    c: Cursor
+) : HashMap<String, Any?>(), ItemWithTitle<String>, ItemWithImage<Any>, ItemWithUri<Uri> {
 
     override val title: String?
         get() = this[MediaStore.Video.Media.TITLE]?.toString()
@@ -51,7 +53,7 @@ class VideoItem(
         }
 
     init {
-        putAll(ci)
+        putAll(c.asMap(MEDIA_PROJECTION))
     }
 
     companion object {

+ 3 - 4
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/AppsRow.kt

@@ -10,7 +10,6 @@ package org.mjdev.tvapp.base.ui.components.tv
 
 import android.content.Context
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
@@ -38,11 +37,11 @@ fun AppsRow(
     },
 ) {
     val context: Context = LocalContext.current
-    val apps = rememberAppsManager().collectAsState(emptyList())
-    if (apps.value.isNotEmpty()) {
+    val apps = rememberAppsManager()
+    if (apps.isNotEmpty()) {
         CategoryRow(
             title = title,
-            items = apps.value,
+            items = apps,
             rowState = rowState,
             padding = padding,
             cardHeight = 80.dp,

+ 84 - 27
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/CursorRow.kt

@@ -8,22 +8,41 @@
 
 package org.mjdev.tvapp.base.ui.components.tv
 
+import android.annotation.SuppressLint
 import android.content.Context
+import android.database.Cursor
 import android.net.Uri
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
 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 androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardHeight
-import org.mjdev.tvapp.base.helpers.cursor.CursorItem
-import org.mjdev.tvapp.base.helpers.cursor.rememberMediaCursor
+import org.mjdev.tvapp.base.extensions.ModifierExt.recomposeHighlighter
+import org.mjdev.tvapp.base.helpers.cursor.rememberCursor
+import org.mjdev.tvapp.base.ui.components.card.PhotoCard
+import org.mjdev.tvapp.base.ui.components.text.TextAny
 
+@SuppressLint("AutoboxingStateValueProperty")
+@OptIn(ExperimentalTvMaterial3Api::class)
 @TvPreview
 @Composable
 fun CursorRow(
@@ -31,36 +50,74 @@ fun CursorRow(
     rowState: TvLazyListState = rememberTvLazyListState(),
     padding: Dp = 16.dp,
     backgroundColor: Color = Color.DarkGray,
+    cardHeight: Dp = computeCardHeight(),
+    contentScale: ContentScale = ContentScale.Crop,
     uri: Uri? = null,
     projection: Array<String> = emptyArray(),
     selection: String? = null,
     selectionArgs: Array<String>? = null,
     sortOrder: String? = null,
-    transform: (CursorItem) -> Any? = { it },
-    cardHeight: Dp = computeCardHeight(),
-    contentScale: ContentScale = ContentScale.Crop,
-    openItem: Context.(item: CursorItem?) -> Unit = {},
+    prefetchItems: Int = 8,
+    transform: (Cursor) -> Any? = { it },
+    openItem: Context.(item: Any?) -> Unit = {},
 ) {
     val context: Context = LocalContext.current
-    val items = rememberMediaCursor(
-        uri,
-        projection,
-        selection,
-        selectionArgs,
-        sortOrder
-    ).collectAsState(emptyList())
-    if (items.value.isNotEmpty()) {
-        CategoryRow(
-            title = title,
-            items = items.value.map { item -> transform(item) },
-            rowState = rowState,
-            padding = padding,
-            cardHeight = cardHeight,
-            contentScale = contentScale,
-            backgroundColor = backgroundColor,
-            onItemClick = { item ->
-                openItem(context, item as? CursorItem)
-            },
-        )
+    val cursor = rememberCursor(
+        uri = uri,
+        projection = projection,
+        selection = selection,
+        selectionArgs = selectionArgs,
+        sortOrder = sortOrder,
+        prefetchItems = prefetchItems,
+        transform = transform
+    )
+    val scrollDelta = remember { mutableFloatStateOf(0f) }
+    if (cursor?.isNotEmpty == true) {
+        Column(
+            modifier = Modifier
+                .recomposeHighlighter()
+                .fillMaxWidth()
+                .background(backgroundColor, RectangleShape)
+        ) {
+            TextAny(
+                modifier = Modifier
+                    .recomposeHighlighter()
+                    .fillMaxWidth()
+                    .padding(padding / 2, padding / 2),
+                text = title,
+                color = Color.White,
+                fontWeight = FontWeight.Bold
+            )
+            TVRow(
+                horizontalArrangement = Arrangement.spacedBy(padding),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .pointerInput(Unit) {
+                        detectDragGestures { _, dragAmount ->
+                            scrollDelta.value = dragAmount.x
+                        }
+                    }
+                    .recomposeHighlighter(),
+                state = rowState,
+                contentPadding = (padding.value * 1.3).dp, // todo due border & glow
+            ) {
+                items(
+                    count = cursor.count,
+                ) { idx ->
+                    val item = cursor.get(idx)
+                    PhotoCard(
+                        item = item,
+                        contentScale = contentScale,
+                        cardHeight = cardHeight,
+                        onClick = {
+                            openItem(context, item)
+                        },
+                    )
+                }
+            }
+            LaunchedEffect(scrollDelta.value) {
+                rowState.dispatchRawDelta(-scrollDelta.value)
+            }
+        }
     }
 }

+ 4 - 5
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/LocalAudioRow.kt

@@ -18,10 +18,9 @@ import androidx.tv.foundation.lazy.list.TvLazyListState
 import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
-import org.mjdev.tvapp.base.extensions.ComposeExt
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardHeight
+import org.mjdev.tvapp.base.extensions.ComposeExt.rememberContentResolver
 import org.mjdev.tvapp.base.helpers.cursor.AudioItem
-import org.mjdev.tvapp.base.helpers.cursor.CursorItem
 
 @TvPreview
 @Composable
@@ -35,9 +34,9 @@ fun LocalAudioRow(
     sortOrder: String? = null,
     cardHeight: Dp = computeCardHeight(),
     contentScale: ContentScale = ContentScale.Crop,
-    openItem: Context.(item: CursorItem?) -> Unit = {},
+    openItem: Context.(item: Any?) -> Unit = {},
 ) {
-    val contentResolver = ComposeExt.rememberContentResolver()
+    val contentResolver = rememberContentResolver()
     CursorRow(
         title = title,
         rowState = rowState,
@@ -50,7 +49,7 @@ fun LocalAudioRow(
         selection = selection,
         selectionArgs = selectionArgs,
         sortOrder = sortOrder,
-        transform = { AudioItem(contentResolver, it) },
+        transform = { c -> AudioItem(contentResolver, c) },
         openItem = openItem,
     )
 }

+ 1 - 2
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/LocalPhotosRow.kt

@@ -19,7 +19,6 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardHeight
-import org.mjdev.tvapp.base.helpers.cursor.CursorItem
 import org.mjdev.tvapp.base.helpers.cursor.PhotoItem
 
 @TvPreview
@@ -34,7 +33,7 @@ fun LocalPhotosRow(
     sortOrder: String? = null,
     cardHeight: Dp = computeCardHeight(),
     contentScale: ContentScale = ContentScale.Crop,
-    openItem: Context.(item: CursorItem?) -> Unit = {},
+    openItem: Context.(item: Any?) -> Unit = {},
 ) = CursorRow(
     title = title,
     rowState = rowState,

+ 1 - 2
app/src/main/kotlin/org/mjdev/tvapp/base/ui/components/tv/LocalVideoRow.kt

@@ -20,7 +20,6 @@ import org.mjdev.tvapp.R
 import org.mjdev.tvapp.base.annotations.TvPreview
 import org.mjdev.tvapp.base.extensions.ComposeExt.computeCardHeight
 import org.mjdev.tvapp.base.extensions.ComposeExt.rememberContentResolver
-import org.mjdev.tvapp.base.helpers.cursor.CursorItem
 import org.mjdev.tvapp.base.helpers.cursor.VideoItem
 
 @TvPreview
@@ -35,7 +34,7 @@ fun LocalVideoRow(
     sortOrder: String? = null,
     cardHeight: Dp = computeCardHeight(),
     contentScale: ContentScale = ContentScale.Crop,
-    openItem: Context.(item: CursorItem?) -> Unit = {},
+    openItem: Context.(item: Any?) -> Unit = {},
 ) {
     val contentResolver = rememberContentResolver()
     CursorRow(