Milan Jurkulak пре 3 година
родитељ
комит
9326b8b6c0

+ 4 - 5
app/src/main/kotlin/org/mjdev/tvapp/activity/IPTVActivity.kt

@@ -18,6 +18,7 @@ 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.tvapp.ui.screens.LoadingScreen
 import org.mjdev.tvlib.extensions.NavControllerExt.openAsTop
 import org.mjdev.tvlib.interfaces.ItemAudio
 import org.mjdev.tvlib.interfaces.ItemPhoto
@@ -34,14 +35,12 @@ class IPTVActivity : ComposableActivity() {
     override val navGraphBuilder: NavGraphBuilderEx.() -> Unit = {
 
         screen(
-            route = IPTVScreen(),
+            route = LoadingScreen(),
             isHomeScreen = true,
             isStartScreen = true,
         )
-
-        screen(
-            route = DetailScreen()
-        )
+        screen(route = IPTVScreen())
+        screen(route = DetailScreen())
 
     }
 

+ 3 - 1
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/DetailScreen.kt

@@ -22,6 +22,8 @@ import org.mjdev.tvapp.R
 import org.mjdev.tvapp.viewmodel.DetailViewModel
 import org.mjdev.tvlib.annotations.TvPreview
 import org.mjdev.tvlib.extensions.HiltExt.appViewModel
+import org.mjdev.tvlib.extensions.ListExt.indexOf
+import org.mjdev.tvlib.extensions.MediaItemExt.uri
 import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.screen.Screen
 import org.mjdev.tvlib.ui.components.gallery.Gallery
@@ -60,7 +62,7 @@ class DetailScreen : Screen() {
         Gallery(
             modifier = Modifier.fillMaxSize(),
             list = dataList,
-            index = dataList.indexOf(data)
+            index = dataList.indexOf<Any?> { item -> item.uri == data.uri },
         )
 
     }

+ 18 - 15
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/IPTVScreen.kt

@@ -9,7 +9,6 @@
 package org.mjdev.tvapp.ui.screens
 
 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.Tv
@@ -19,6 +18,7 @@ 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.media3.common.MediaItem
 import androidx.navigation.navArgument
 import org.mjdev.tvapp.R
 import org.mjdev.tvapp.viewmodel.DetailViewModel
@@ -27,8 +27,11 @@ import org.mjdev.tvlib.extensions.HiltExt.appViewModel
 import org.mjdev.tvlib.navigation.AnyType
 import org.mjdev.tvlib.screen.Screen
 import org.mjdev.tvlib.ui.components.media.MediaPlayerContainer
-import org.mjdev.tvlib.extensions.MediaItemExt.mediaItem
+import org.mjdev.tvlib.extensions.MediaItemExt.uri
+import org.mjdev.tvlib.extensions.ListExt.indexOf
+import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMediaPlayerState
 
+@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
 class IPTVScreen : Screen() {
 
     private val data = "data"
@@ -53,25 +56,25 @@ class IPTVScreen : Screen() {
             DetailViewModel.mockDetailViewModel(context)
         }
 
-        val data: Any? = remember { args[data] }
-
-        val dataList: List<Any?> = remember { viewModel.mediaItemsFor(data) }
+        val data: Any? by lazy {
+            args[data]
+        }
 
-        val itemIndex = remember {
-            dataList.indexOf(data)
+        val dataList: List<MediaItem> = remember(data) {
+            viewModel.mediaItemsFor(data)
         }
 
-        Box(
+        val state = rememberMediaPlayerState(
+            items = dataList,
+            itemToPlay = dataList.indexOf<Any?> { item -> item.uri == data.uri },
+        )
+
+        MediaPlayerContainer(
             modifier = Modifier
                 .fillMaxSize()
                 .background(Color.Black, RectangleShape),
-        ) {
-            MediaPlayerContainer(
-                modifier = Modifier.fillMaxSize(),
-                items = dataList.map { item -> item.mediaItem },
-                itemToPlay = itemIndex
-            )
-        }
+            state = state
+        )
 
     }
 

+ 55 - 0
app/src/main/kotlin/org/mjdev/tvapp/ui/screens/LoadingScreen.kt

@@ -0,0 +1,55 @@
+/*
+ *  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.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.Tv
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+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.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import org.mjdev.tvapp.R
+import org.mjdev.tvlib.annotations.TvPreview
+import org.mjdev.tvlib.screen.Screen
+import org.mjdev.tvlib.ui.components.text.TextAny
+
+class LoadingScreen : Screen() {
+
+    override val title = R.string.title_loading
+    override val menuIcon: ImageVector get() = Icons.Filled.Tv
+    override val immersive: Boolean = true
+    override val showOnce: Boolean = true
+
+    @TvPreview
+    @Composable
+    override fun ComposeScreen() {
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(Color.Black, RectangleShape),
+            contentAlignment = Alignment.Center,
+        ) {
+            TextAny(
+                text = stringResource(id = title),
+                color = Color.White,
+                fontSize = 32.sp,
+                fontWeight = FontWeight.Bold
+            )
+        }
+    }
+
+}

+ 19 - 11
app/src/main/kotlin/org/mjdev/tvapp/viewmodel/DetailViewModel.kt

@@ -26,6 +26,7 @@ import org.mjdev.tvlib.interfaces.ItemPhoto
 import org.mjdev.tvlib.interfaces.ItemVideo
 import org.mjdev.tvlib.network.NetworkConnectivityServiceImpl
 import javax.inject.Inject
+import kotlin.reflect.KClass
 
 @Suppress("MemberVisibilityCanBePrivate")
 @HiltViewModel
@@ -46,21 +47,28 @@ class DetailViewModel @Inject constructor() : BaseViewModel() {
     @Inject
     lateinit var localPhotoCursor: PhotoCursor
 
-    val mediaItems: List<MediaItem> by lazy {
-        dao.movieDao.all.map { item ->
-            item.mediaItem
-        }
-    }
+    val mediaItems: List<Any> get() = dao.movieDao.all
+
+    val cache = mutableMapOf<KClass<*>, List<MediaItem>>()
 
     // todo this should be improved
     fun mediaItemsFor(
         data: Any?
-    ): List<Any?> = when (data) {
-        is ItemAudio -> localAudioCursor
-        is ItemVideo -> localVideoCursor
-        is ItemPhoto -> localPhotoCursor
-        is Movie -> mediaItems
-        else -> listOf(data)
+    ): List<MediaItem> = when (data) {
+        is ItemAudio -> getCachedList<ItemAudio> { localAudioCursor }
+        is ItemVideo -> getCachedList<ItemVideo> { localVideoCursor }
+        is ItemPhoto -> getCachedList<ItemPhoto> { localPhotoCursor }
+        is Movie -> getCachedList<Movie> { mediaItems }
+        else -> listOf(data.mediaItem)
+    }
+
+    private inline fun <reified T> getCachedList(creator: () -> List<*>): List<MediaItem> {
+        if (!cache.containsKey(T::class)) {
+            creator().let { list ->
+                cache[T::class] = list.map { item -> item.mediaItem }
+            }
+        }
+        return cache[T::class] ?: emptyList()
     }
 
     companion object {

+ 1 - 0
app/src/main/res/values-cs/strings.xml

@@ -40,6 +40,7 @@
     <string name="title_photo_local">Lokálni fotografie</string>
     <string name="title_iptv">Televize</string>
     <string name="title_web">Internet</string>
+    <string name="title_loading">Loading</string>
 
     <string name="author">Vytvorené autorem Milan Jurkulák © 2023</string>
 

+ 1 - 0
app/src/main/res/values-sk/strings.xml

@@ -40,6 +40,7 @@
     <string name="title_photo_local">Lokálne fotografie</string>
     <string name="title_iptv">Televízia</string>
     <string name="title_web">Internet</string>
+    <string name="title_loading">Loading</string>
 
     <string name="author">Vytvorené autorom Milan Jurkulák © 2023</string>
 

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -40,6 +40,7 @@
     <string name="title_photo_local">Local photo files</string>
     <string name="title_iptv">Television</string>
     <string name="title_web">Internet</string>
+    <string name="title_loading">Loading</string>
 
     <string name="author">Created by Milan Jurkulak © 2023</string>
 

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

@@ -60,7 +60,9 @@ open class ComposableActivity : ComponentActivity() {
     open val roundCornerSize: Dp = 0.dp
     open val backgroundShape: Shape = RoundedCornerShape(roundCornerSize)
 
-    lateinit var navController : NavHostControllerEx
+    lateinit var navController: NavHostControllerEx
+
+    var lastIntent: Intent? = null
 
     @TvPreview
     @OptIn(ExperimentalTvMaterial3Api::class)
@@ -95,13 +97,22 @@ open class ComposableActivity : ComponentActivity() {
                 )
             }
         }
-        onIntent(navController, intent)
+        if (intent != null) {
+            if ((lastIntent == null) || (lastIntent != intent)) {
+                lastIntent = intent
+                onIntent(navController, intent)
+            }
+        }
     }
 
-    // todo
     override fun onNewIntent(intent: Intent?) {
         super.onNewIntent(intent)
-        onIntent(navController, intent)
+        if (intent != null) {
+            if ((lastIntent == null) || (lastIntent != intent)) {
+                lastIntent = intent
+                onIntent(navController, intent)
+            }
+        }
     }
 
     override fun onConfigurationChanged(newConfig: Configuration) {

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

@@ -16,6 +16,7 @@ import android.content.res.Configuration
 import android.os.Build
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.State
 import androidx.compose.runtime.derivedStateOf
@@ -38,6 +39,7 @@ import coil.decode.SvgDecoder
 import coil.decode.VideoFrameDecoder
 import coil.disk.DiskCache
 import coil.request.CachePolicy
+import kotlinx.coroutines.CoroutineScope
 import okhttp3.Cache
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
@@ -78,6 +80,13 @@ object ComposeExt {
         derivedStateOf(function)
     }
 
+    @SuppressLint("ComposableNaming")
+    @Composable
+    fun runOnceOnStart(
+        key: Any?,
+        block: suspend CoroutineScope.() -> Unit
+    ) = LaunchedEffect(key, block)
+
     @Composable
     fun <T> textFrom(text: T?): String = when (text) {
         null -> ""

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

@@ -109,7 +109,7 @@ object HiltExt {
     }
 
     @Suppress("UNUSED_PARAMETER")
-    fun createInternal(
+    private fun createInternal(
         activity: Activity,
         owner: SavedStateRegistryOwner,
         defaultArgs: Bundle?,
@@ -118,7 +118,7 @@ object HiltExt {
         return createInternal(activity, delegateFactory)
     }
 
-    fun createInternal(
+    private fun createInternal(
         activity: Activity, delegateFactory: ViewModelProvider.Factory
     ): ViewModelProvider.Factory {
         val entryPoint = EntryPoints.get(

+ 6 - 2
tvlib/src/main/kotlin/org/mjdev/tvlib/extensions/MediaItemExt.kt

@@ -39,7 +39,11 @@ object MediaItemExt {
         context.metadataRetriever.getInfo(this)
 
     val Any?.uri: String
-        get() = ((this as? ItemWithUri<*>)?.uri ?: this).toString()
+        get() = when (this) {
+            is MediaItem -> this.localConfiguration.uri
+            is ItemWithUri<*> -> (this as? ItemWithUri<*>)?.uri.toString()
+            else -> this.toString()
+        }
 
     val Any?.mediaType: Int
         get() = when (this) {
@@ -66,7 +70,7 @@ object MediaItemExt {
             .build()
 
     val Any?.mediaItem: MediaItem
-        get() = MediaItem.Builder()
+        get() = if (this is MediaItem) this else MediaItem.Builder()
             .setUri(uri)
             .setMediaMetadata(metaData)
             .build()

+ 13 - 47
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerContainer.kt

@@ -11,17 +11,14 @@
 package org.mjdev.tvlib.ui.components.media
 
 import android.annotation.SuppressLint
-import android.content.Context
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.statusBarsPadding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.media3.common.MediaItem
 import org.mjdev.tvlib.annotations.TvPreview
-import org.mjdev.tvlib.extensions.ComposeExt.isEditMode
 import org.mjdev.tvlib.extensions.ModifierExt.recomposeHighlighter
 import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMediaPlayerState
 
@@ -30,59 +27,28 @@ import org.mjdev.tvlib.ui.components.media.MediaPlayerState.Companion.rememberMe
 @Composable
 fun MediaPlayerContainer(
     modifier: Modifier = Modifier,
-    autoPlay: Boolean = true,
-    startSeek: Long = 0L,
-    context: Context = LocalContext.current,
-    isEdit: Boolean = isEditMode(),
-    items: List<MediaItem> = emptyList(),
-    itemToPlay: Int = 0,
-    mediaPlayer: IMediaPlayer = remember {
-        if (isEdit) {
-            IMediaPlayer.EMPTY
-        } else {
-            MediaPlayerContainerDefaults.exoPlayer(context)
-        }
-    },
-    state: MediaPlayerState = rememberMediaPlayerState(
-        player = mediaPlayer,
-        items = items,
-        itemToPlay = itemToPlay,
-        autoPlay = autoPlay,
-        startSeek = startSeek,
-    )
+    state: MediaPlayerState = rememberMediaPlayerState(),
 ) {
     Box(
         modifier = modifier
             .recomposeHighlighter()
             .fillMaxSize()
     ) {
-        mediaPlayer.GetPlayerView(
+        state.player.GetPlayerView(
+            modifier = modifier.recomposeHighlighter()
+        )
+        PlayerControlView(
             modifier = modifier
                 .recomposeHighlighter()
-                .fillMaxSize()
+                .navigationBarsPadding()
+                .statusBarsPadding()
+                .fillMaxSize(),
+            state = state
         )
-        if (isEdit) {
-            PlayerControlView(
-                modifier = modifier
-                    .recomposeHighlighter()
-                    .fillMaxSize()
-            )
-        }
     }
-    DisposableEffect(state) {
-        if (state.hasMediaToPlay) {
-            mediaPlayer.setMediaItems(
-                state.mediaItems.value,
-                state.currentItemIndex.intValue,
-                state.currentPosition.longValue
-            )
-            mediaPlayer.prepare()
-            if (state.isAutoPlay) {
-                mediaPlayer.play()
-            }
-        }
+    DisposableEffect(Unit) {
+        state.startPlay()
         onDispose {
-            state.stop()
             state.dispose()
         }
     }

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

@@ -9,9 +9,8 @@
 package org.mjdev.tvlib.ui.components.media
 
 import android.content.Context
-import androidx.media3.common.util.UnstableApi
 
-@UnstableApi
+@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 object MediaPlayerContainerDefaults {
 

+ 151 - 39
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/MediaPlayerState.kt

@@ -8,108 +8,220 @@
 
 package org.mjdev.tvlib.ui.components.media
 
+import android.content.Context
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableLongStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.platform.LocalContext
+import androidx.media3.common.DeviceInfo
 import androidx.media3.common.MediaItem
 import androidx.media3.common.PlaybackException
 import androidx.media3.common.Player
+import androidx.media3.common.text.CueGroup
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.ToJson
+import org.json.JSONObject
 import org.mjdev.tvlib.extensions.MediaItemExt.mediaItem
+import org.mjdev.tvlib.extensions.MediaItemExt.uri
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
+@JsonClass(generateAdapter = true)
 class MediaPlayerState(
-    val player: IMediaPlayer,
-    initItems: List<MediaItem>,
+    initItems: List<MediaItem> = emptyList(),
     initItemIndex: Int = 0,
     autoPlay: Boolean = true,
-    startSeek: Long = 0,
+    startSeek: Long? = null,
 ) : Player.Listener {
 
-    val mediaItems: MutableState<List<MediaItem>> = mutableStateOf(
-        initItems
-    )
+    @Json(name = "mediaItems")
+    var mediaItems: List<MediaItem> = initItems
 
-    val currentItemIndex = mutableIntStateOf(initItemIndex)
+    @Json(name = "currentItemIndex")
+    var currentItemIndex = initItemIndex
 
-    val currentItem = derivedStateOf {
-        mediaItems.value[currentItemIndex.intValue].mediaItem
-    }
+    @Json(name = "currentPosition")
+    var currentPosition = startSeek ?: 0L
 
-    val currentPosition = mutableLongStateOf(startSeek)
+    @Json(name = "isPlaying")
+    var isPlaying: Boolean = autoPlay
 
-    val isPlaying: MutableState<Boolean> = mutableStateOf(autoPlay)
+    @Json(name = "error", ignore = true)
+    var error: Exception? = null
 
-    val error: MutableState<Exception?> = mutableStateOf(null)
+    @Json(name = "hasMediaToPlay", ignore = true)
+    val hasMediaToPlay get() = mediaItems.isNotEmpty()
 
-    val hasMediaToPlay = mediaItems.value.isNotEmpty()
+    @Json(name = "isAutoPlay", ignore = true)
+    val isAutoPlay get() = isPlaying
 
-    val isAutoPlay = isPlaying.value
+    @Json(name = "currentItem", ignore = true)
+    val currentItem get() = mediaItems[currentItemIndex].mediaItem
 
-    init {
-        player.addListener(this)
-    }
+    @Json(name = "player", ignore = true)
+    var player: IMediaPlayer = IMediaPlayer.EMPTY
+        set(value) {
+            field.stop()
+            field.release()
+            field.dispose()
+            field.removeListener(this)
+            field = value
+            field.addListener(this)
+        }
 
     fun play() {
-        isPlaying.value = true
+        isPlaying = true
+        player.play()
     }
 
     fun stop() {
-        isPlaying.value = false
+        isPlaying = false
+        player.stop()
     }
 
     fun pause() {
-        isPlaying.value = false
+        isPlaying = false
+        player.pause()
     }
 
     fun resume() {
-        isPlaying.value = true
+        isPlaying = true
+        player.resume()
     }
 
     fun seekTo(ms: Long) {
-        currentPosition.longValue = ms
+        currentPosition = ms
+        player.seekTo(ms)
     }
 
     fun dispose() {
+        if (player.isPlaying) {
+            player.stop()
+        }
         player.release()
+        player.dispose()
     }
 
     override fun onIsPlayingChanged(isPlaying: Boolean) {
-        this.isPlaying.value = isPlaying
+        this.isPlaying = isPlaying
     }
 
     override fun onPlayerError(error: PlaybackException) {
-        this.error.value = error
+        this.error = error
     }
 
     override fun onPlayerErrorChanged(error: PlaybackException?) {
-        this.error.value = error
+        this.error = error
     }
 
     override fun onPlaybackStateChanged(playbackState: Int) {
-        currentPosition.longValue = player.currentPosition
+        currentPosition = player.currentPosition
+    }
+
+    override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {
+        currentPosition = player.currentPosition
+    }
+
+    override fun onSeekForwardIncrementChanged(seekForwardIncrementMs: Long) {
+        currentPosition = player.currentPosition
+    }
+
+    override fun onSeekBackIncrementChanged(seekBackIncrementMs: Long) {
+        currentPosition = player.currentPosition
+    }
+
+    override fun onCues(cueGroup: CueGroup) {
+        currentPosition = player.currentPosition
+    }
+
+    override fun onMaxSeekToPreviousPositionChanged(maxSeekToPreviousPositionMs: Long) {
+        currentPosition = player.currentPosition
+    }
+
+    fun startPlay() {
+        val mediaItems = mediaItems
+        val currentItemIndex = currentItemIndex
+        val currentPosition = currentPosition
+        val isAutoPlay = isAutoPlay
+        if (hasMediaToPlay) {
+            player.setMediaItems(
+                mediaItems,
+                currentItemIndex,
+                currentPosition
+            )
+            player.prepare()
+            player.playWhenReady = isAutoPlay
+        }
     }
 
     companion object {
 
         @Composable
         fun rememberMediaPlayerState(
-            player: IMediaPlayer,
-            items: List<MediaItem>,
+            items: List<MediaItem> = listOf(),
             itemToPlay: Int = 0,
             autoPlay: Boolean = true,
-            startSeek: Long = 0,
-        ) = remember {
+            startSeek: Long? = null,
+            context: Context = LocalContext.current,
+        ) = rememberSaveable(
+            key = MediaPlayerState::class.simpleName,
+            inputs = arrayOf(items, itemToPlay),
+            saver = mediaPlayerStateSaver(context),
+        ) {
             MediaPlayerState(
-                player,
                 items,
                 itemToPlay,
                 autoPlay,
                 startSeek,
-            )
+            ).apply {
+                player = MediaPlayerContainerDefaults.exoPlayer(context)
+            }
+        }
+
+        fun mediaPlayerStateSaver(
+            context: Context
+        ): Saver<MediaPlayerState, String> = Saver(
+            save = { mps ->
+                Moshi.Builder()
+                    .add(MediaItemAdapter())
+                    .build()
+                    .adapter(MediaPlayerState::class.java)
+                    .toJson(mps)
+            },
+            restore = { json ->
+                Moshi.Builder()
+                    .add(MediaItemAdapter())
+                    .build()
+                    .adapter(MediaPlayerState::class.java)
+                    .fromJson(json)?.apply {
+                        player = MediaPlayerContainerDefaults.exoPlayer(context)
+                    }
+            }
+        )
+
+        // todo
+        class MediaItemAdapter {
+            // todo
+            @ToJson
+            fun toJson(item: MediaItem): String {
+                return StringBuilder()
+                    .append("{")
+                    .append("  ", "uri", "=", "\"", item.uri, "\"")
+                    .append("}")
+                    .toString()
+            }
+
+            // todo
+            @FromJson
+            fun fromJson(json: String): MediaItem {
+                return JSONObject(json).let { obj ->
+                    MediaItem.Builder()
+                        .setUri(obj.getString("uri"))
+                        .build()
+                }
+            }
         }
 
     }

+ 24 - 10
tvlib/src/main/kotlin/org/mjdev/tvlib/ui/components/media/PlayerControlView.kt

@@ -10,18 +10,32 @@ package org.mjdev.tvlib.ui.components.media
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.media3.ui.PlayerControlView
+import org.mjdev.tvlib.annotations.TvPreview
 
+@TvPreview
 @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
 @Composable
-fun  PlayerControlView(
-    modifier: Modifier = Modifier
+fun PlayerControlView(
+    modifier: Modifier = Modifier,
+    state: MediaPlayerState = MediaPlayerState.rememberMediaPlayerState(),
 ) {
-    AndroidView(
-        modifier = modifier,
-        factory = { context ->
-            PlayerControlView(context)
-        }
-    )
+//    val isEdit = isEditMode()
+//    val infoVisible = remember { mutableStateOf(isEdit) }
+//    AndroidView(
+//        modifier = modifier.conditional(isEdit) {
+//            fillMaxSize()
+//        },
+//        factory = { context ->
+//            PlayerControlView(context).apply {
+//                addVisibilityListener { visibility ->
+//                    infoVisible.value = (visibility == View.VISIBLE)
+//                }
+//                setPlayer(player)
+//            }
+//        }
+//    )
+//    ItemInfo(
+//        src = item,
+//        infoVisible = infoVisible
+//    )
 }