Преглед изворни кода

player improvements & detail improvements

Milan Jurkulak пре 3 година
родитељ
комит
8d314d175c
23 измењених фајлова са 251 додато и 452 уклоњено
  1. 5 1
      app/build.gradle.kts
  2. 33 2
      app/src/main/kotlin/org/mjdev/tvapp/activity/IPTVActivity.kt
  3. 0 4
      app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt
  4. 0 53
      app/src/main/kotlin/org/mjdev/tvapp/ui/components/MediaPlayer.kt
  5. 7 25
      app/src/main/kotlin/org/mjdev/tvapp/ui/pages/MainPage.kt
  6. 43 72
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt
  7. 32 7
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt
  8. 0 79
      app/src/main/kotlin/org/mjdev/tvapp/ui/screens/PlayerScreen.kt
  9. 0 81
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/DetailViewModel.kt
  10. 5 0
      app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt
  11. 5 1
      tvlib/build.gradle.kts
  12. 6 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt
  13. 4 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/ComposeExt.kt
  14. 2 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioCursor.kt
  15. 24 33
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioItem.kt
  16. 1 3
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoCursor.kt
  17. 40 37
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoItem.kt
  18. 2 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoCursor.kt
  19. 24 33
      tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoItem.kt
  20. 14 0
      tvlib/src/main/kotlin/org/mjdev/tvlib/navigation/NavHostControllerEx.kt
  21. 1 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalAudioRow.kt
  22. 1 4
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalPhotosRow.kt
  23. 2 5
      tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalVideoRow.kt

+ 5 - 1
app/build.gradle.kts

@@ -25,13 +25,13 @@ val SIGNING_CONFIG_NAME = "Any"
 plugins {
     id("com.android.application")
     kotlin("android")
-    kotlin("kapt")
     id("kotlin-parcelize")
     id("com.google.dagger.hilt.android")
     id("dagger.hilt.android.plugin")
 //    id("org.jetbrains.dokka") version "1.8.10"
     id("io.objectbox")
     id("MainAppPlugin")
+    kotlin("kapt")
 }
 
 android {
@@ -164,6 +164,10 @@ android {
         checkReleaseBuilds = false
     }
 
+    hilt {
+        enableAggregatingTask = true
+    }
+
 //    subprojects {
 //        apply(plugin = "org.jetbrains.dokka")
 //    }

+ 33 - 2
app/src/main/kotlin/org/mjdev/tvapp/activity/IPTVActivity.kt

@@ -8,15 +8,22 @@
 
 package org.mjdev.tvapp.activity
 
+import android.content.Intent
 import androidx.compose.runtime.Composable
 import dagger.hilt.android.AndroidEntryPoint
+import org.mjdev.tvapp.data.local.Movie
+import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvlib.activity.ComposableActivity
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
 import org.mjdev.tvapp.ui.screens.IPTVScreen
+import org.mjdev.tvlib.extensions.NavControllerExt.openAsTop
+import org.mjdev.tvlib.interfaces.ItemAudio
+import org.mjdev.tvlib.interfaces.ItemPhoto
+import org.mjdev.tvlib.interfaces.ItemVideo
+import org.mjdev.tvlib.navigation.NavHostControllerEx
 
-// todo
 @AndroidEntryPoint
 class IPTVActivity : ComposableActivity() {
 
@@ -28,8 +35,32 @@ class IPTVActivity : ComposableActivity() {
 
         screen(
             route = IPTVScreen(),
-            isHomeScreen = true
+            isHomeScreen = true,
+            isStartScreen = true,
         )
 
+        screen(
+            route = DetailScreen()
+        )
+
+    }
+
+    @Suppress("DEPRECATION", "MoveVariableDeclarationIntoWhen")
+    override fun onIntent(navController: NavHostControllerEx, intent: Intent?) {
+        val data = intent?.getSerializableExtra(IPTV_DATA)
+        when (data) {
+            is Movie -> navController.openAsTop<IPTVScreen>(data)
+            is ItemAudio -> navController.openAsTop<IPTVScreen>(data)
+            is ItemVideo -> navController.openAsTop<IPTVScreen>(data)
+            is ItemPhoto -> navController.openAsTop<DetailScreen>(data)
+            else -> navController.openAsTop<DetailScreen>(data)
+        }
     }
+
+    companion object {
+
+        const val IPTV_DATA = "IPTV_DATA"
+
+    }
+
 }

+ 0 - 4
app/src/main/kotlin/org/mjdev/tvapp/activity/MainActivity.kt

@@ -14,9 +14,7 @@ import org.mjdev.tvlib.activity.ComposableActivity
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.NavGraphBuilderExt.screen
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
-import org.mjdev.tvapp.ui.screens.DetailScreen
 import org.mjdev.tvapp.ui.screens.MainScreen
-import org.mjdev.tvapp.ui.screens.PlayerScreen
 import org.mjdev.tvapp.ui.screens.SplashScreen
 
 @AndroidEntryPoint
@@ -30,8 +28,6 @@ class MainActivity : ComposableActivity() {
 
         screen(route = SplashScreen(), isStartScreen = true)
         screen(route = MainScreen(), isHomeScreen = true)
-        screen(route = DetailScreen())
-        screen(route = PlayerScreen())
 
     }
 }

+ 0 - 53
app/src/main/kotlin/org/mjdev/tvapp/ui/components/MediaPlayer.kt

@@ -1,53 +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.ui.components
-
-import android.annotation.SuppressLint
-import android.net.Uri
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.RectangleShape
-import org.mjdev.tvlib.annotations.TvPreview
-import org.mjdev.tvlib.extensions.StringExt.parseUri
-import org.mjdev.tvlib.interfaces.ItemWithUri
-import org.mjdev.tvlib.ui.components.media.MediaPlayerContainer
-import java.net.URL
-
-@SuppressLint("ModifierParameter")
-@TvPreview
-@Composable
-fun MediaPlayer(
-    data: Any? = null, modifier: Modifier = Modifier
-) {
-
-    val uriToPlay: String? = when (data) {
-        null -> null
-        is ItemWithUri<*> -> data.uri.toString()
-        is String -> data.toString()
-        is URL -> data.toString()
-        is Uri -> data.toString()
-        else -> data.toString()
-    }
-
-    Box(
-        modifier = modifier
-            .fillMaxSize()
-            .background(Color.Black, RectangleShape),
-    ) {
-        MediaPlayerContainer(
-            modifier = modifier.fillMaxSize(),
-            uri = uriToPlay?.parseUri(),
-        )
-    }
-
-}

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

@@ -23,9 +23,10 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
 import org.mjdev.tvapp.R
+import org.mjdev.tvapp.activity.IPTVActivity
+import org.mjdev.tvapp.activity.IPTVActivity.Companion.IPTV_DATA
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.HiltExt.appViewModel
-import org.mjdev.tvlib.extensions.NavControllerExt.open
 import org.mjdev.tvlib.interfaces.ItemWithId
 import org.mjdev.tvlib.ui.components.page.Page
 import org.mjdev.tvlib.ui.components.tv.AppsRow
@@ -33,15 +34,12 @@ import org.mjdev.tvlib.ui.components.tv.BrowseView
 import org.mjdev.tvlib.ui.components.tv.LocalAudioRow
 import org.mjdev.tvlib.ui.components.tv.LocalPhotosRow
 import org.mjdev.tvlib.ui.components.tv.LocalVideoRow
-import org.mjdev.tvapp.ui.screens.DetailScreen
-import org.mjdev.tvapp.ui.screens.PlayerScreen
 import org.mjdev.tvapp.viewmodel.MainViewModel
-import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemPhoto
-import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.ui.components.image.FadingPhotoImage
+import java.io.Serializable
 import java.net.URL
 
 @SuppressLint("ComposableNaming")
@@ -70,27 +68,11 @@ class MainPage : Page() {
         val onItemClick: (item: Any?) -> Unit = { item ->
             val dataId = (item as? ItemWithId)?.id
             if (dataId != null) {
-                navController?.open<PlayerScreen>(dataId)
-            } else {
-                when (item) {
-                    is ItemAudio -> {
-                        val dataUri = (item as? ItemAudio)?.uri
-                        navController?.open<PlayerScreen>(dataUri)
-                    }
-
-                    is ItemVideo -> {
-                        val dataUri = (item as? ItemVideo)?.uri
-                        navController?.open<PlayerScreen>(dataUri)
-                    }
-
-                    is ItemPhoto -> {
-                        navController?.open<DetailScreen>(item)
-                    }
-
-                    else -> {
-                        navController?.open<DetailScreen>(item)
-                    }
+                viewModel.findMovie(dataId)?.let { movie ->
+                    navController?.startActivity<IPTVActivity>(IPTV_DATA to movie)
                 }
+            } else if (item is Serializable) {
+                navController?.startActivity<IPTVActivity>(IPTV_DATA to item)
             }
         }
 

+ 43 - 72
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt

@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Info
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -34,12 +33,7 @@ import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.ContentScale
 import androidx.navigation.navArgument
 import org.mjdev.tvapp.R
-import org.mjdev.tvapp.base.ui.components.tv.Loading
-import org.mjdev.tvapp.state.DetailsLoadingState
-import org.mjdev.tvapp.viewmodel.DetailViewModel
 import org.mjdev.tvlib.annotations.TvPreview
-import org.mjdev.tvlib.extensions.HiltExt.appViewModel
-import org.mjdev.tvlib.extensions.StringExt.asException
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithDescription
@@ -73,16 +67,8 @@ class DetailScreen : Screen() {
     @Composable
     override fun ComposeScreen() {
 
-        val viewModel: DetailViewModel = appViewModel { context ->
-            DetailViewModel.mockDetailViewModel(context)
-        }
-
         val data: Any? = args[data]
 
-        val dataState = remember {
-            viewModel.detailsLoadingState(data)
-        }.collectAsState()
-
         val photo = (data as? ItemPhoto)?.uri
         val backgroundImage = (data as? ItemWithBackground<*>)?.background
         val image = (data as? ItemWithImage<*>)?.image
@@ -91,69 +77,54 @@ class DetailScreen : Screen() {
         val description = (data as? ItemWithDescription<*>)?.description
         var infoVisible by remember { mutableStateOf(true) }
 
-        fun postError(error: String) = viewModel.postError(error.asException())
-
-        when (dataState.value) {
-            is DetailsLoadingState.Ready -> {
-                Box(
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .pointerInput(Unit) {
+                    detectTapGestures {
+                        infoVisible = !infoVisible
+                    }
+                },
+            contentAlignment = Alignment.BottomStart,
+        ) {
+            ImageAny(
+                src = photo ?: backgroundImage ?: image,
+                contentDescription = null,
+                contentScale = ContentScale.Crop,
+                modifier = Modifier.fillMaxSize()
+            )
+            AnimatedVisibility(
+                visible = infoVisible,
+                enter = fadeIn(),
+                exit = fadeOut(),
+            ) {
+                Column(
                     modifier = Modifier
-                        .fillMaxSize()
-                        .pointerInput(Unit) {
-                            detectTapGestures {
-                                infoVisible = !infoVisible
-                            }
-                        },
-                    contentAlignment = Alignment.BottomStart,
+                        .fillMaxWidth()
+                        .alpha(0.8f)
+                        .background(themeProvider.detailTextBackgroundColor)
+                        .padding(
+                            vertical = themeProvider.detailTextPaddingVertical,
+                            horizontal = themeProvider.detailTextPaddingHorizontal
+                        )
                 ) {
-                    ImageAny(
-                        src = photo ?: backgroundImage ?: image,
-                        contentDescription = null,
-                        contentScale = ContentScale.Crop,
-                        modifier = Modifier.fillMaxSize()
+                    TextAny(
+                        text = title ?: "-",
+                        style = themeProvider.detailsTitleTextStyle,
+                        color = themeProvider.detailsTextColor
+                    )
+                    TextAny(
+                        text = subtitle ?: "-",
+                        style = themeProvider.detailsTextStyle,
+                        color = themeProvider.detailsTextColor
+                    )
+                    TextAny(
+                        text = description ?: "-",
+                        style = themeProvider.detailsTextStyle,
+                        color = themeProvider.detailsTextColor
                     )
-                    AnimatedVisibility(
-                        visible = infoVisible,
-                        enter = fadeIn(),
-                        exit = fadeOut(),
-                    ) {
-                        Column(
-                            modifier = Modifier
-                                .fillMaxWidth()
-                                .alpha(0.8f)
-                                .background(themeProvider.detailTextBackgroundColor)
-                                .padding(
-                                    vertical = themeProvider.detailTextPaddingVertical,
-                                    horizontal = themeProvider.detailTextPaddingHorizontal
-                                )
-                        ) {
-                            TextAny(
-                                text = title ?: "-",
-                                style = themeProvider.detailsTitleTextStyle,
-                                color = themeProvider.detailsTextColor
-                            )
-                            TextAny(
-                                text = subtitle ?: "-",
-                                style = themeProvider.detailsTextStyle,
-                                color = themeProvider.detailsTextColor
-                            )
-                            TextAny(
-                                text = description ?: "-",
-                                style = themeProvider.detailsTextStyle,
-                                color = themeProvider.detailsTextColor
-                            )
-                        }
-                    }
                 }
             }
-
-            DetailsLoadingState.Loading -> {
-                Loading()
-            }
-
-            else -> {
-                Loading()
-                postError("File or address $data not found.")
-            }
         }
 
     }

+ 32 - 7
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt

@@ -8,27 +8,37 @@
 
 package org.mjdev.tvapp.ui.screens
 
+import android.net.Uri
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.Tv
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.navigation.navArgument
 import org.mjdev.tvapp.R
-import org.mjdev.tvapp.ui.components.MediaPlayer
 import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.extensions.StringExt.parseUri
+import org.mjdev.tvlib.interfaces.ItemWithUri
 import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.screen.Screen
+import org.mjdev.tvlib.ui.components.media.MediaPlayerContainer
+import java.net.URL
 
+@Suppress("MoveVariableDeclarationIntoWhen")
 class IPTVScreen : Screen() {
 
     private val data = "data"
 
     override val title = R.string.title_iptv
-    override val menuIcon: ImageVector get() = Icons.Filled.PlayArrow
+    override val menuIcon: ImageVector get() = Icons.Filled.Tv
     override val immersive: Boolean = true
+    override val showOnce: Boolean = true
 
     override val routeArgs = listOf(
         navArgument(data) {
@@ -43,10 +53,25 @@ class IPTVScreen : Screen() {
 
         val data: Any? = remember { args[data] }
 
-        MediaPlayer(
-            modifier = Modifier.fillMaxSize(),
-            data = data
-        )
+        val uriToPlay: String? = when (data) {
+            null -> null
+            is ItemWithUri<*> -> data.uri.toString()
+            is String -> data.toString()
+            is URL -> data.toString()
+            is Uri -> data.toString()
+            else -> data.toString()
+        }
+
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(Color.Black, RectangleShape),
+        ) {
+            MediaPlayerContainer(
+                modifier = Modifier.fillMaxSize(),
+                uri = uriToPlay?.parseUri(),
+            )
+        }
 
     }
 

+ 0 - 79
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/PlayerScreen.kt

@@ -1,79 +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.ui.screens
-
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.PlayArrow
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.navigation.navArgument
-import org.mjdev.tvapp.R
-import org.mjdev.tvapp.base.ui.components.tv.Loading
-import org.mjdev.tvapp.state.DetailsLoadingState
-import org.mjdev.tvapp.ui.components.MediaPlayer
-import org.mjdev.tvapp.viewmodel.DetailViewModel
-import org.mjdev.tvlib.annotations.TvPreview
-import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
-import org.mjdev.tvlib.extensions.HiltExt.appViewModel
-import org.mjdev.tvlib.extensions.StringExt.asException
-import org.mjdev.tvlib.navigation.AnyType
-import org.mjdev.tvlib.screen.Screen
-
-class PlayerScreen : Screen() {
-
-    private val data = "data"
-
-    override val title = R.string.title_movie_player
-    override val menuIcon: ImageVector get() = Icons.Filled.PlayArrow
-    override val immersive: Boolean = true
-
-    override val routeArgs = listOf(
-        navArgument(data) {
-            nullable = true
-            type = AnyType
-        }
-    )
-
-    @TvPreview
-    @Composable
-    override fun ComposeScreen() {
-
-        val isEdit = isEditMode()
-
-        val viewModel: DetailViewModel = appViewModel { context ->
-            DetailViewModel.mockDetailViewModel(context)
-        }
-
-        val data: Any? = remember { args[data] }
-        val dataState = remember {
-            viewModel.detailsLoadingState(data)
-        }.collectAsState()
-
-        fun postError(error: String) = viewModel.postError(error.asException())
-
-        if (isEdit || (dataState.value is DetailsLoadingState.Ready)) {
-            MediaPlayer(
-                modifier = Modifier.fillMaxSize(),
-                data = (dataState.value as? DetailsLoadingState.Ready)?.data
-            )
-        } else if (dataState.value is DetailsLoadingState.NotFound) {
-            postError("Can not play: ${dataState.value}. File not found.")
-            Loading()
-        } else {
-            postError("Can not play: ${dataState.value}. File unknown type.")
-            Loading()
-        }
-
-    }
-
-}

+ 0 - 81
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/DetailViewModel.kt

@@ -1,81 +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.viewmodel
-
-import android.content.Context
-import android.net.Uri
-import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.stateIn
-import org.mjdev.tvapp.database.DAO
-import org.mjdev.tvlib.viewmodel.BaseViewModel
-import org.mjdev.tvapp.repository.IMovieRepository
-import org.mjdev.tvapp.repository.MovieRepository
-import org.mjdev.tvapp.state.DetailsLoadingState
-import java.net.URL
-import javax.inject.Inject
-
-@HiltViewModel
-class DetailViewModel @Inject constructor() : BaseViewModel() {
-
-    @Inject
-    lateinit var movieRepository: IMovieRepository
-
-    fun detailsLoadingState(
-        data: Any?
-    ): StateFlow<DetailsLoadingState> = flow {
-        try {
-            when (data) {
-                is Int, is Long -> {
-                    val movieId: Long? = data as? Long
-                    val result = movieRepository.findMovieById(movieId)
-                    val state = if (result.isSuccess) {
-                        DetailsLoadingState.Ready(result.getOrThrow())
-                    } else {
-                        DetailsLoadingState.NotFound
-                    }
-                    emit(state)
-                }
-
-                is String -> {
-                    emit(DetailsLoadingState.Ready(data))
-                }
-
-                is Uri, is URL -> {
-                    emit(DetailsLoadingState.Ready(data))
-                }
-
-                else -> {
-                    emit(DetailsLoadingState.Ready(data))
-                }
-            }
-        } catch (e: Exception) {
-            emit(DetailsLoadingState.Error(e))
-        }
-    }.stateIn(
-        viewModelScope,
-        SharingStarted.WhileSubscribed(5000L),
-        DetailsLoadingState.Loading
-    )
-
-    companion object {
-
-        @Suppress("unused")
-        fun mockDetailViewModel(
-            context: Context
-        ): DetailViewModel = DetailViewModel().apply {
-            movieRepository = MovieRepository(DAO(context))
-        }
-
-    }
-
-}

+ 5 - 0
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/MainViewModel.kt

@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.runBlocking
 import org.mjdev.tvlib.extensions.ListExt.asMap
 import org.mjdev.tvlib.helpers.apps.AppsManager
 import org.mjdev.tvlib.helpers.cursor.AudioCursor
@@ -109,6 +110,10 @@ class MainViewModel @Inject constructor() : BaseViewModel() {
         listOf()
     )
 
+    fun findMovie(id: Long?): Movie? = runBlocking {
+        movieRepository.findMovieById(id).getOrNull()
+    }
+
     companion object {
 
         @Suppress("unused")

+ 5 - 1
tvlib/build.gradle.kts

@@ -13,10 +13,10 @@ import org.mjdev.gradle.plugin.MainAppPlugin.Companion.kotlinCompilerExtVersion
 plugins {
     id("com.android.library")
     kotlin("android")
-    kotlin("kapt")
     id("com.google.dagger.hilt.android")
     id("dagger.hilt.android.plugin")
 //    id("org.jetbrains.dokka") version "1.8.10"
+    kotlin("kapt")
 }
 
 android {
@@ -66,6 +66,10 @@ android {
         checkReleaseBuilds = false
     }
 
+    hilt {
+        enableAggregatingTask = true
+    }
+
 //    tasks.dokkaGfm {
 //        outputDirectory.set(File(projectDir, "../wiki/documentation"))
 //    }

+ 6 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/activity/ComposableActivity.kt

@@ -37,6 +37,7 @@ import org.mjdev.tvlib.helpers.anr.agent.ANRSpyAgent
 import org.mjdev.tvlib.helpers.anr.agent.ANRSpyListener
 import org.mjdev.tvlib.helpers.anr.models.MethodModel
 import org.mjdev.tvlib.navigation.NavGraphBuilderEx
+import org.mjdev.tvlib.navigation.NavHostControllerEx
 import org.mjdev.tvlib.ui.components.navigation.NavHostEx
 import org.mjdev.tvlib.ui.components.screen.EmptyScreen
 import timber.log.Timber
@@ -114,6 +115,11 @@ open class ComposableActivity : ComponentActivity() {
             }
         }
 
+        onIntent(navController, intent)
+
+    }
+
+    open fun onIntent(navController: NavHostControllerEx, intent: Intent?) {
     }
 
     override fun onCreate(savedInstanceState: Bundle?) {

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

@@ -32,6 +32,8 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api
 import coil.ImageLoader
 import coil.decode.GifDecoder
 import coil.decode.ImageDecoderDecoder
+import coil.decode.SvgDecoder
+import coil.decode.VideoFrameDecoder
 import coil.request.CachePolicy
 import org.mjdev.tvlib.extensions.ContextExt.isATv
 
@@ -148,6 +150,8 @@ object ComposeExt {
                     } else {
                         add(GifDecoder.Factory())
                     }
+                    add(VideoFrameDecoder.Factory())
+                    add(SvgDecoder.Factory())
                 }
                 .build()
         }

+ 2 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioCursor.kt

@@ -8,16 +8,14 @@
 
 package org.mjdev.tvlib.helpers.cursor
 
-import android.content.ContentResolver
 import android.content.Context
 
 class AudioCursor(
-    context: Context,
-    val contentResolver: ContentResolver = context.contentResolver
+    context: Context
 ) : CachingCursor(
     context = context,
     uri = AudioItem.URI,
     projection = AudioItem.MEDIA_PROJECTION,
-    transform = { AudioItem(contentResolver, it) },
+    transform = { c -> AudioItem(c) },
     sortOrder = AudioItem.SORT_ORDER_DATE_DESC,
 )

+ 24 - 33
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/AudioItem.kt

@@ -8,61 +8,51 @@
 
 package org.mjdev.tvlib.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.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithUri
-import timber.log.Timber
+import java.io.Serializable
 
 @Suppress("unused")
-class AudioItem(
-    private val contentResolver: ContentResolver,
-    c: Cursor
-) : HashMap<String, Any?>(), ItemAudio, ItemWithTitle<String>, ItemWithImage<Any>, ItemWithUri<Uri> {
+class AudioItem() :
+    Serializable,
+    ItemAudio,
+    ItemWithTitle<String>,
+    ItemWithImage<Any>,
+    ItemWithUri<String> {
 
-    init {
-        putAll(c.asMap(MEDIA_PROJECTION))
-    }
+    override var title: String? = null
 
-    override val title: String?
-        get() = this[MediaStore.Audio.Media.TITLE]?.toString()
+    override var uri: String? = null
 
-    override val uri: Uri
-        get() = Uri.withAppendedPath(
-            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
-            this[MediaStore.Audio.Media._ID].toString()
-        )
+    override var image: Any? = null
 
-    override val image: Any?
-        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            try {
-                uri.let { thumbUri ->
-                    contentResolver.loadThumbnail(
-                        thumbUri,
-                        Size(320, 240),
-                        null
-                    )
-                }
-            } catch (e: Exception) {
-                Timber.e(e)
-                null
+    constructor(c: Cursor) : this() {
+        c.asMap(VideoItem.MEDIA_PROJECTION).forEach { vp ->
+            when (vp.key) {
+                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()
             }
-        } else {
-            Uri.parse(this[MediaStore.Video.Media.DATA].toString())
         }
+    }
 
     companion object {
+
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Audio.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_ASC: String = MediaStore.Audio.Media.DATE_ADDED + " ASC"
 
         val URI: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+
         val MEDIA_PROJECTION = arrayOf(
             MediaStore.Audio.Media._ID,
             MediaStore.Audio.Media.TITLE,
@@ -70,6 +60,7 @@ class AudioItem(
             MediaStore.Audio.Media.DATE_ADDED,
             // todo more columns
         )
+
     }
 
 }

+ 1 - 3
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoCursor.kt

@@ -8,16 +8,14 @@
 
 package org.mjdev.tvlib.helpers.cursor
 
-import android.content.ContentResolver
 import android.content.Context
 
 class PhotoCursor(
     context: Context,
-    val contentResolver: ContentResolver = context.contentResolver
 ) : CachingCursor(
     context = context,
     uri = PhotoItem.URI,
     projection = PhotoItem.MEDIA_PROJECTION,
-    transform = { PhotoItem(contentResolver, it) },
+    transform = { c -> PhotoItem(c) },
     sortOrder = PhotoItem.SORT_ORDER_DATE_DESC,
 )

+ 40 - 37
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/PhotoItem.kt

@@ -8,69 +8,71 @@
 
 package org.mjdev.tvlib.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.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemWithBackground
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
-import timber.log.Timber
+import java.io.Serializable
 
 @Suppress("unused")
-class PhotoItem(
-    private val contentResolver: ContentResolver,
-    c: Cursor
-) :
-    HashMap<String, Any?>(),
+class PhotoItem() :
+    Serializable,
     ItemPhoto,
     ItemWithTitle<String>,
     ItemWithImage<Any>,
     ItemWithBackground<Any?> {
 
-    init {
-        putAll(c.asMap(MEDIA_PROJECTION))
-    }
-
-    override val title: String?
-        get() = this[MediaStore.Images.Media.TITLE]?.toString()
+    override var title: String? = null
+    override var uri: String? = null
+    override var image: Any? = null
+    override var background: Any? = null
 
-    override val uri: Uri
-        get() = Uri.withAppendedPath(
-            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
-            this[MediaStore.Images.Media._ID].toString()
-        )
+    constructor(c: Cursor) : this() {
+        c.asMap(VideoItem.MEDIA_PROJECTION).forEach { vp ->
+            when (vp.key) {
+                MediaStore.Images.Media.TITLE -> title = vp.value.toString()
+                MediaStore.Images.Media._ID -> uri = Uri.withAppendedPath(
+                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                    vp.value.toString()
+                ).toString()
 
-    override val background: Any?
-        get() = this[MediaStore.Images.Media.DATA]
-
-    override val image: Any?
-        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            try {
-                uri.let { thumbUri ->
-                    contentResolver.loadThumbnail(
-                        thumbUri,
-                        Size(320, 240),
-                        null
-                    )
+                MediaStore.Images.Media.DATA -> {
+                    image = vp.value.toString()
+                    background = vp.value.toString()
                 }
-            } catch (e: Exception) {
-                Timber.e(e)
-                null
             }
-        } else {
-            Uri.parse(this[MediaStore.Images.Media.DATA].toString())
         }
+    }
+
+//    override val image: Any?
+//        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+//            try {
+//                uri.let { thumbUri ->
+//                    contentResolver.loadThumbnail(
+//                        thumbUri,
+//                        Size(320, 240),
+//                        null
+//                    )
+//                }
+//            } catch (e: Exception) {
+//                Timber.e(e)
+//                null
+//            }
+//        } else {
+//            Uri.parse(this[MediaStore.Images.Media.DATA].toString())
+//        }
 
     companion object {
+
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Images.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_ASC: String = MediaStore.Images.Media.DATE_ADDED + " ASC"
 
         val URI: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+
         val MEDIA_PROJECTION = arrayOf(
             MediaStore.Images.Media._ID,
             MediaStore.Images.Media.TITLE,
@@ -78,6 +80,7 @@ class PhotoItem(
             MediaStore.Images.Media.DATE_ADDED,
             // todo more columns
         )
+
     }
 
 }

+ 2 - 4
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoCursor.kt

@@ -8,16 +8,14 @@
 
 package org.mjdev.tvlib.helpers.cursor
 
-import android.content.ContentResolver
 import android.content.Context
 
 class VideoCursor(
-    context: Context,
-    val contentResolver: ContentResolver = context.contentResolver
+    context: Context
 ) : CachingCursor(
     context = context,
     uri = VideoItem.URI,
     projection = VideoItem.MEDIA_PROJECTION,
-    transform = { VideoItem(contentResolver, it) },
+    transform = { c -> VideoItem(c) },
     sortOrder = VideoItem.SORT_ORDER_DATE_DESC,
 )

+ 24 - 33
tvlib/src/main/kotlin/org/mjdev/tvlib/helpers/cursor/VideoItem.kt

@@ -8,61 +8,51 @@
 
 package org.mjdev.tvlib.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.tvlib.extensions.CursorExt.asMap
 import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.interfaces.ItemWithImage
 import org.mjdev.tvlib.interfaces.ItemWithTitle
 import org.mjdev.tvlib.interfaces.ItemWithUri
-import timber.log.Timber
+import java.io.Serializable
 
 @Suppress("unused")
-class VideoItem(
-    private val contentResolver: ContentResolver,
-    c: Cursor
-) : HashMap<String, Any?>(), ItemVideo, ItemWithTitle<String>, ItemWithImage<Any>, ItemWithUri<Uri> {
+class VideoItem() :
+    Serializable,
+    ItemVideo,
+    ItemWithTitle<String>,
+    ItemWithImage<Any>,
+    ItemWithUri<String> {
 
-    override val title: String?
-        get() = this[MediaStore.Video.Media.TITLE]?.toString()
+    override var title: String? = null
 
-    override val uri: Uri
-        get() = Uri.withAppendedPath(
-            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-            this[MediaStore.Video.Media._ID].toString()
-        )
+    override var uri: String? = null
+
+    override var image: Any? = null
 
-    override val image: Any?
-        get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
-            try {
-                uri.let { thumbUri ->
-                    contentResolver.loadThumbnail(
-                        thumbUri,
-                        Size(320, 240),
-                        null
-                    )
-                }
-            } catch (e: Exception) {
-                Timber.e(e)
-                null
+    constructor(c: Cursor) : this() {
+        c.asMap(MEDIA_PROJECTION).forEach { vp ->
+            when (vp.key) {
+                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()
             }
-        } else {
-            Uri.parse(this[MediaStore.Video.Media.DATA].toString())
         }
-
-    init {
-        putAll(c.asMap(MEDIA_PROJECTION))
     }
 
     companion object {
+
         const val SORT_ORDER_DATE_DESC: String = MediaStore.Video.Media.DATE_ADDED + " DESC"
         const val SORT_ORDER_DATE_ASC: String = MediaStore.Video.Media.DATE_ADDED + " ASC"
 
         val URI: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
+
         val MEDIA_PROJECTION = arrayOf(
             MediaStore.Video.Media._ID,
             MediaStore.Video.Media.TITLE,
@@ -70,6 +60,7 @@ class VideoItem(
             MediaStore.Video.Media.DATE_ADDED
             // todo more columns
         )
+
     }
 
 }

+ 14 - 0
tvlib/src/main/kotlin/org/mjdev/tvlib/navigation/NavHostControllerEx.kt

@@ -9,12 +9,15 @@
 package org.mjdev.tvlib.navigation
 
 import android.annotation.SuppressLint
+import android.app.Activity
 import android.content.Context
+import android.content.Intent
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.core.os.bundleOf
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -24,6 +27,7 @@ import androidx.tv.material3.DrawerState
 import androidx.tv.material3.DrawerValue
 import androidx.tv.material3.ExperimentalTvMaterial3Api
 import org.mjdev.tvlib.extensions.ListExt.addUnique
+import java.io.Serializable
 
 // todo apply lifecycle
 @Suppress("unused", "MemberVisibilityCanBePrivate")
@@ -129,4 +133,14 @@ class NavHostControllerEx(
 
     val isMenuEnabled: Boolean get() = menuState.value
 
+    inline fun <reified T : Activity> startActivity(
+        vararg params: Pair<String, Serializable?>
+    ) {
+        Intent(context, T::class.java).apply {
+            putExtras(bundleOf(*params))
+        }.also { i ->
+            context.startActivity(i)
+        }
+    }
+
 }

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

@@ -8,7 +8,6 @@
 
 package org.mjdev.tvlib.ui.components.tv
 
-import android.content.ContentResolver
 import android.content.Context
 import android.database.Cursor
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -23,7 +22,6 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
-import org.mjdev.tvlib.extensions.ComposeExt.rememberContentResolver
 import org.mjdev.tvlib.helpers.cursor.AudioItem
 import org.mjdev.tvlib.helpers.cursor.CachingCursor.Companion.rememberCursor
 
@@ -42,8 +40,7 @@ fun LocalAudioRow(
     sortOrder: String? = AudioItem.SORT_ORDER_DATE_DESC,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
-    contentResolver: ContentResolver = rememberContentResolver(),
-    transform: (Cursor) -> Any? = { c -> AudioItem(contentResolver, c) },
+    transform: (Cursor) -> Any? = { c -> AudioItem(c) },
     cursor: Cursor? = rememberCursor(
         uri = AudioItem.URI,
         projection = AudioItem.MEDIA_PROJECTION,

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

@@ -8,7 +8,6 @@
 
 package org.mjdev.tvlib.ui.components.tv
 
-import android.content.ContentResolver
 import android.content.Context
 import android.database.Cursor
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -23,7 +22,6 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
-import org.mjdev.tvlib.extensions.ComposeExt.rememberContentResolver
 import org.mjdev.tvlib.helpers.cursor.CachingCursor.Companion.rememberCursor
 import org.mjdev.tvlib.helpers.cursor.PhotoItem
 
@@ -42,8 +40,7 @@ fun LocalPhotosRow(
     sortOrder: String? = PhotoItem.SORT_ORDER_DATE_DESC,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
-    contentResolver: ContentResolver = rememberContentResolver(),
-    transform: (Cursor) -> Any? = { c -> PhotoItem(contentResolver, c) },
+    transform: (Cursor) -> Any? = { c -> PhotoItem(c) },
     cursor: Cursor? = rememberCursor(
         uri = PhotoItem.URI,
         projection = PhotoItem.MEDIA_PROJECTION,

+ 2 - 5
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/tv/LocalVideoRow.kt

@@ -8,7 +8,6 @@
 
 package org.mjdev.tvlib.ui.components.tv
 
-import android.content.ContentResolver
 import android.content.Context
 import android.database.Cursor
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -23,7 +22,6 @@ import androidx.tv.foundation.lazy.list.rememberTvLazyListState
 import org.mjdev.tvlib.R
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.ComposeExt.computeCardWidth
-import org.mjdev.tvlib.extensions.ComposeExt.rememberContentResolver
 import org.mjdev.tvlib.helpers.cursor.CachingCursor.Companion.rememberCursor
 import org.mjdev.tvlib.helpers.cursor.VideoItem
 
@@ -42,8 +40,7 @@ fun LocalVideoRow(
     sortOrder: String? = VideoItem.SORT_ORDER_DATE_DESC,
     cardWidth: Dp = computeCardWidth(),
     contentScale: ContentScale = ContentScale.Crop,
-    contentResolver: ContentResolver = rememberContentResolver(),
-    transform: (Cursor) -> Any? = { c -> VideoItem(contentResolver, c) },
+    transform: (Cursor) -> Any? = { c -> VideoItem(c) },
     cursor: Cursor? = rememberCursor(
         uri = VideoItem.URI,
         projection = VideoItem.MEDIA_PROJECTION,
@@ -69,7 +66,7 @@ fun LocalVideoRow(
         selection = selection,
         selectionArgs = selectionArgs,
         sortOrder = sortOrder,
-        transform = { c -> VideoItem(contentResolver, c) },
+        transform = { c -> VideoItem(c) },
         openItem = openItem,
         onItemFocus = onItemFocus,
         cursor = cursor