make playlist items draggable

This commit is contained in:
Christian Schabesberger 2024-09-03 12:48:16 +02:00
parent 38bf37e88a
commit 8ed25f5039
11 changed files with 172 additions and 281 deletions

View File

@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="TestApp">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-07-23T15:40:12.466915691Z">
<DropdownSelection timestamp="2024-09-02T13:31:45.216735120Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=981f7af2" />

View File

@ -43,6 +43,7 @@ kotlinParcelize = "2.0.20-Beta2"
newplayer = "master-SNAPSHOT"
okhttpAndroid = "5.0.0-alpha.14"
coil = "2.7.0"
reorderable = "2.4.0-alpha02"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -77,6 +78,7 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit
newplayer = { group = "com.github.theScrabi.NewPlayer", name = "new-player", version.ref = "newplayer" }
okhttp-android = { group = "com.squareup.okhttp3", name = "okhttp-android", version.ref = "okhttpAndroid" }
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }

View File

@ -67,6 +67,7 @@ dependencies {
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.media3.common)
implementation(libs.coil.compose)
implementation(libs.reorderable)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)

View File

@ -23,7 +23,6 @@ package net.newpipe.newplayer
import android.app.Application
import android.util.Log
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Timeline
@ -40,10 +39,11 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.newpipe.newplayer.playerInternals.PlayList
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromItemList
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromExoplayer
import net.newpipe.newplayer.utils.Thumbnail
import kotlin.Exception
import kotlin.random.Random
enum class PlayMode {
EMBEDDED_VIDEO,
@ -69,8 +69,7 @@ interface NewPlayer {
var playBackMode: PlayMode
var playMode: StateFlow<PlayMode?>
val playlist: PlayList
val playlistInPlaylistItems: StateFlow<List<PlaylistItem>>
val playlist: StateFlow<List<PlaylistItem>>
// callbacks
@ -82,6 +81,8 @@ interface NewPlayer {
fun play()
fun pause()
fun addToPlaylist(item: String)
fun movePlaylistItem(fromIndex: Int, toIndex: Int)
fun removePlaylistItem(index: Int)
fun playStream(item: String, playMode: PlayMode)
fun playStream(item: String, streamVariant: String, playMode: PlayMode)
fun setPlayMode(playMode: PlayMode)
@ -131,6 +132,8 @@ class NewPlayerImpl(
override val sharingLinkWithOffsetPossible: Boolean
) : NewPlayer {
private var uniqueIdToIdLookup = HashMap<Long, String>()
var mutableErrorFlow = MutableSharedFlow<Exception>()
override val errorFlow = mutableErrorFlow.asSharedFlow()
@ -165,11 +168,9 @@ class NewPlayerImpl(
override val duration: Long
get() = internalPlayer.duration
override val playlist = PlayList(internalPlayer)
val mutablePlaylistAsPlaylistItems = MutableStateFlow<List<PlaylistItem>>(emptyList())
override val playlistInPlaylistItems: StateFlow<List<PlaylistItem>> =
mutablePlaylistAsPlaylistItems.asStateFlow()
val mutablePlaylist = MutableStateFlow<List<PlaylistItem>>(emptyList())
override val playlist: StateFlow<List<PlaylistItem>> =
mutablePlaylist.asStateFlow()
init {
println("gurken init")
@ -202,13 +203,13 @@ class NewPlayerImpl(
private fun updatePlaylistItems() {
playerScope.launch {
val playlist = getPlaylistItemsFromItemList(playlist, repository)
val playlist = getPlaylistItemsFromExoplayer(internalPlayer, repository, uniqueIdToIdLookup)
var playlistDuration = 0
for (item in playlist) {
playlistDuration += item.lengthInS
}
mutablePlaylistAsPlaylistItems.update {
mutablePlaylist.update {
playlist
}
}
@ -237,6 +238,14 @@ class NewPlayerImpl(
}
}
override fun movePlaylistItem(fromIndex: Int, toIndex: Int) {
internalPlayer.moveMediaItem(fromIndex, toIndex)
}
override fun removePlaylistItem(index: Int) {
internalPlayer.removeMediaItem(index)
}
override fun playStream(item: String, playMode: PlayMode) {
launchJobAndCollectError {
val mediaItem = toMediaItem(item)
@ -266,7 +275,9 @@ class NewPlayerImpl(
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {
val dataStream = repository.getStream(item, streamVariant)
val mediaItem = MediaItem.Builder().setMediaId(item).setUri(dataStream)
val uniqueId = Random.nextLong()
uniqueIdToIdLookup.set(uniqueId, item)
val mediaItem = MediaItem.Builder().setMediaId(uniqueId.toString()).setUri(dataStream)
return mediaItem.build()
}

View File

@ -63,4 +63,6 @@ interface VideoPlayerViewModel {
fun setRepeatmode(repeatMode: Int)
fun setSuffleEnabled(enabled: Boolean)
fun onStorePlaylist()
fun movePlaylistItem(from: Int, to: Int)
fun removePlaylistItem(index: Int)
}

View File

@ -30,7 +30,6 @@ import androidx.core.content.ContextCompat.getSystemService
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
@ -46,7 +45,6 @@ import kotlinx.coroutines.launch
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.utils.VideoSize
import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.playerInternals.getPlaylistItemsFromItemList
import net.newpipe.newplayer.ui.ContentScale
val VIDEOPLAYER_UI_STATE = "video_player_ui_state"
@ -172,7 +170,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
}
}
viewModelScope.launch {
newPlayer.playlistInPlaylistItems.collect { playlist ->
newPlayer.playlist.collect { playlist ->
mutableUiState.update { it.copy(playList = playlist) }
}
}
@ -423,6 +421,14 @@ class VideoPlayerViewModelImpl @Inject constructor(
TODO("Not yet implemented")
}
override fun movePlaylistItem(from: Int, to: Int) {
newPlayer?.movePlaylistItem(from, to)
}
override fun removePlaylistItem(index: Int) {
newPlayer?.removePlaylistItem(index)
}
private fun updateUiMode(newState: UIModeState) {
val newPlayMode = newState.toPlayMode()
val currentPlayMode = mutableUiState.value.uiMode.toPlayMode()

View File

@ -100,7 +100,15 @@ open class VideoPlayerViewModelDummy : VideoPlayerViewModel {
}
override fun onStorePlaylist() {
TODO("Not yet implemented")
println("dummy impl")
}
override fun movePlaylistItem(from: Int, to: Int) {
println("dummy impl")
}
override fun removePlaylistItem(index: Int) {
println("dummy impl")
}
override fun pause() {

View File

@ -1,162 +0,0 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*
*/
package net.newpipe.newplayer.playerInternals
import androidx.media3.common.Player
// TODO: This is cool, but it might still contains all raceconditions since two actors are mutating the
// same time.
// Be aware when using this list and this iterator: There can alwas be out of bounds exceptions
// even if the size in a previous query said otherwise, since between the size query and
// a get element query the count of elements might have been changed by exoplayer itself
// due to this reason some functions force the user to handle elements out of bounds exceptions.
class PlayListIterator(
val exoPlayer: Player,
val fromIndex: Int,
val toIndex: Int
) : ListIterator<String> {
var index = fromIndex
override fun hasNext() =
index < minOf(exoPlayer.mediaItemCount, toIndex)
override fun hasPrevious() = fromIndex < index
@Throws(IndexOutOfBoundsException::class)
override fun next(): String {
if (exoPlayer.mediaItemCount <= index)
throw NoSuchElementException("No Stream with index $index in the playlist")
val item = exoPlayer.getMediaItemAt(index).mediaId
index++
return item
}
@Throws(IndexOutOfBoundsException::class)
override fun nextIndex() =
if (exoPlayer.mediaItemCount <= index)
exoPlayer.mediaItemCount - fromIndex
else
(index + 1) - fromIndex
@Throws(IndexOutOfBoundsException::class)
override fun previous(): String {
if (index <= fromIndex)
throw NoSuchElementException("No Stream with index ${index - 1} in the playlist")
index--
val item = exoPlayer.getMediaItemAt(index).mediaId
return item
}
override fun previousIndex() =
if (index <= fromIndex)
0
else
(index - 1) - fromIndex
}
class PlayList(val exoPlayer: Player, val fromIndex: Int = 0, val toIndex: Int = Int.MAX_VALUE) :
List<String> {
override val size: Int
get() = minOf(exoPlayer.mediaItemCount, toIndex) - fromIndex
// TODO: This contains a race condition. When the player might change the playlist while this function runns
override fun contains(element: String): Boolean {
for (i in fromIndex..minOf(exoPlayer.mediaItemCount, toIndex)) {
try {
if (exoPlayer.getMediaItemAt(i).mediaId == element)
return true
} catch (e: IndexOutOfBoundsException) {
return false
}
}
return false
}
// TODO: This contains a race condition. When the player might change the playlist while this function runns
override fun containsAll(elements: Collection<String>): Boolean {
for (element in elements) {
if (!this.contains(element)) {
return false
}
}
return true
}
@Throws(IndexOutOfBoundsException::class)
override fun get(index: Int) =
if (index < 0 || toIndex < index + fromIndex)
throw IndexOutOfBoundsException("Accessed playlist item outside of permitted Playlist ListWindow: $index with [$fromIndex;$toIndex[")
else
exoPlayer.getMediaItemAt(index + fromIndex).mediaId
override fun isEmpty() = exoPlayer.mediaItemCount == 0 || fromIndex == toIndex
override fun iterator() = PlayListIterator(exoPlayer, fromIndex, toIndex)
override fun listIterator() = PlayListIterator(exoPlayer, fromIndex, toIndex)
override fun listIterator(index: Int) = PlayListIterator(exoPlayer, index, toIndex)
override fun subList(fromIndex: Int, toIndex: Int): List<String> =
PlayList(
exoPlayer,
fromIndex = this.fromIndex + fromIndex,
toIndex = this.fromIndex + toIndex
)
override fun lastIndexOf(element: String): Int {
var mediaItemCount = 0
var newMediaItemCount = minOf(toIndex, exoPlayer.mediaItemCount)
// this while loop is there to catch raceconditions
while(mediaItemCount != newMediaItemCount) {
mediaItemCount = newMediaItemCount
for (i in minOf(toIndex, exoPlayer.mediaItemCount) downTo fromIndex) {
try {
if (exoPlayer.getMediaItemAt(i).mediaId == element) {
return i - fromIndex
}
} catch (_: IndexOutOfBoundsException) {}
}
newMediaItemCount = minOf(toIndex, exoPlayer.mediaItemCount)
}
return -1
}
override fun indexOf(element: String): Int {
for (i in fromIndex..minOf(toIndex, exoPlayer.mediaItemCount)) {
try {
if (exoPlayer.getMediaItemAt(i).mediaId == element) {
return i - fromIndex
}
} catch (e: IndexOutOfBoundsException) {
return -1
}
}
return -1
}
}

View File

@ -21,34 +21,48 @@
package net.newpipe.newplayer.playerInternals
import androidx.media3.common.Player
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.utils.Thumbnail
import kotlin.coroutines.coroutineContext
import kotlin.random.Random
data class PlaylistItem(
val title: String,
val creator: String,
val id: String,
val uniqueId: Long,
val thumbnail: Thumbnail?,
val lengthInS: Int
)
suspend fun getPlaylistItemsFromItemList(items: List<String>, mediaRepo: MediaRepository) =
suspend fun getPlaylistItemsFromExoplayer(player: Player, mediaRepo: MediaRepository, idLookupTable: HashMap<Long, String>) =
with(CoroutineScope(coroutineContext)) {
items.map { item ->
(0..player.mediaItemCount-1).map { index ->
println("gurken index: $index")
val mediaItem = player.getMediaItemAt(index)
val uniqueId = mediaItem.mediaId.toLong()
val id = idLookupTable.get(uniqueId)
?: throw NewPlayerException("Unknown uniqueId: $uniqueId, uniqueId Id mapping error. Something went wrong during datafetching.")
Pair(uniqueId, id)
}.map { item ->
Pair(item, async {
mediaRepo.getMetaInfo(item)
mediaRepo.getMetaInfo(item.second)
})
}.map {
val uniqueId = it.first.first
val id = it.first.second
val metaInfo = it.second.await()
PlaylistItem(
title = metaInfo.title,
creator = metaInfo.channelName,
id = it.first,
id = id,
thumbnail = metaInfo.thumbnail,
lengthInS = metaInfo.lengthInS
lengthInS = metaInfo.lengthInS,
uniqueId = uniqueId
)
}
}

View File

@ -20,42 +20,31 @@
package net.newpipe.newplayer.ui.videoplayer
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Repeat
import androidx.compose.material.icons.filled.RepeatOn
import androidx.compose.material.icons.filled.RepeatOne
import androidx.compose.material.icons.filled.RepeatOneOn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.media3.exoplayer.ExoPlayer
import net.newpipe.newplayer.R
import net.newpipe.newplayer.model.VideoPlayerUIState
import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelDummy
import net.newpipe.newplayer.ui.CONTROLLER_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
import net.newpipe.newplayer.Chapter
import net.newpipe.newplayer.NewPlayerException
import net.newpipe.newplayer.playerInternals.PlaylistItem
import net.newpipe.newplayer.ui.STREAMSELECT_UI_BACKGROUND_COLOR
import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterItem
@ -63,6 +52,9 @@ import net.newpipe.newplayer.ui.videoplayer.streamselect.ChapterSelectTopBar
import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamItem
import net.newpipe.newplayer.ui.videoplayer.streamselect.StreamSelectTopBar
import net.newpipe.newplayer.utils.getInsets
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun StreamSelectUI(
@ -91,13 +83,14 @@ fun StreamSelectUI(
}
}
) { innerPadding ->
if (isChapterSelect) {
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
contentPadding = PaddingValues(start = 8.dp, end = 4.dp)
) {
if (isChapterSelect) {
items(uiState.chapters.size) { chapterIndex ->
val chapter = uiState.chapters[chapterIndex]
ChapterItem(
@ -110,27 +103,59 @@ fun StreamSelectUI(
}
)
}
}
} else {
items(uiState.playList.size) { playlistItemIndex ->
val playlistItem = uiState.playList[playlistItemIndex]
StreamItem(
id = playlistItemIndex,
title = playlistItem.title,
creator = playlistItem.creator,
thumbnail = playlistItem.thumbnail,
lengthInMs = playlistItem.lengthInS.toLong() * 1000,
onDragStart = {},
onDragEnd = {},
onClicked = { viewModel.streamSelected(playlistItemIndex) }
ReorderableStreamItemsList(
innerPadding = innerPadding,
viewModel = viewModel,
uiState = uiState
)
}
}
}
}
}
@Composable
fun ReorderableStreamItemsList(
innerPadding: PaddingValues,
viewModel: VideoPlayerViewModel,
uiState: VideoPlayerUIState
) {
val lazyListState = rememberLazyListState()
val reorderableLazyListState =
rememberReorderableLazyListState(lazyListState = lazyListState) { from, to ->
viewModel.movePlaylistItem(from.index, to.index)
}
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
state = lazyListState
) {
itemsIndexed(uiState.playList, key = {_, item -> item.uniqueId}) { index, playlistItem ->
ReorderableItem(
state = reorderableLazyListState,
key = playlistItem.uniqueId
) { isDragging ->
StreamItem(
id = index,
title = playlistItem.title,
creator = playlistItem.creator,
thumbnail = playlistItem.thumbnail,
lengthInMs = playlistItem.lengthInS.toLong() * 1000,
onClicked = { viewModel.streamSelected(it) },
reorderableScope = this@ReorderableItem
)
}
}
}
}
@Preview(device = "id:pixel_5")
@Composable
fun VideoPlayerChannelSelectUIPreview() {
@ -178,21 +203,24 @@ fun VideoPlayerStreamSelectUIPreview() {
title = "Stream 1",
creator = "The Creator",
lengthInS = 6 * 60 + 5,
thumbnail = null
thumbnail = null,
uniqueId = 0
),
PlaylistItem(
id = "6502",
title = "Stream 2",
creator = "The Creator 2",
lengthInS = 2 * 60 + 5,
thumbnail = null
thumbnail = null,
uniqueId = 1
),
PlaylistItem(
id = "6502",
title = "Stream 3",
creator = "The Creator 3",
lengthInS = 29 * 60 + 5,
thumbnail = null
thumbnail = null,
uniqueId = 2
)
)
)

View File

@ -20,33 +20,28 @@
package net.newpipe.newplayer.ui.videoplayer.streamselect
import android.view.MotionEvent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -64,6 +59,7 @@ import net.newpipe.newplayer.utils.Thumbnail
import net.newpipe.newplayer.utils.VectorThumbnail
import net.newpipe.newplayer.utils.getLocale
import net.newpipe.newplayer.utils.getTimeStringFromMs
import sh.calvin.reorderable.ReorderableCollectionItemScope
@Composable
private fun Thumbnail(thumbnail: Thumbnail?, contentDescription: String) {
@ -95,38 +91,6 @@ private fun Thumbnail(thumbnail: Thumbnail?, contentDescription: String) {
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun DragComposable(onDragStart: () -> Unit, onDragEnd: () -> Unit) {
Box(modifier = Modifier
.aspectRatio(1f)
.fillMaxSize()
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_UP -> {
onDragEnd()
false
}
MotionEvent.ACTION_DOWN -> {
onDragStart()
false
}
else -> true
}
}) {
Icon(
modifier = Modifier
.size(25.dp)
.align(Alignment.Center),
imageVector = Icons.Filled.DragHandle,
//contentDescription = stringResource(R.string.stream_item_drag_handle)
contentDescription = "placeholer, TODO: FIXME"
)
}
}
@Composable
fun StreamItem(
modifier: Modifier = Modifier,
@ -135,19 +99,19 @@ fun StreamItem(
creator: String?,
thumbnail: Thumbnail?,
lengthInMs: Long,
onDragStart: (Int) -> Unit,
onDragEnd: (Int) -> Unit,
onClicked: (Int) -> Unit
onClicked: (Int) -> Unit,
reorderableScope: ReorderableCollectionItemScope?
) {
val locale = getLocale()!!
Row(modifier = modifier
.clickable { onClicked(id) }
Row(
modifier = modifier
.padding(5.dp)
.height(IntrinsicSize.Min)) {
.height(60.dp)
) {
Box(
modifier = Modifier
.aspectRatio(16f / 9f)
.wrapContentSize()
.fillMaxSize()
) {
val contentDescription = stringResource(R.string.stream_item_thumbnail)
Thumbnail(thumbnail, contentDescription)
@ -189,11 +153,29 @@ fun StreamItem(
)
}
}
DragComposable(onDragStart = { onDragStart(id) }, onDragEnd = { onDragEnd(id) })
IconButton(
modifier = if (reorderableScope != null) {
with(reorderableScope) {
Modifier
.aspectRatio(1f)
.fillMaxSize()
.draggableHandle()
}
} else {
Modifier
.aspectRatio(1f)
.fillMaxSize()
},
onClick = {}
) {
Icon(
imageVector = Icons.Filled.DragHandle,
contentDescription = stringResource(R.string.stream_item_drag_handle)
)
}
}
}
@Preview(device = "spec:width=1080px,height=400px,dpi=440,orientation=landscape")
@Composable
@ -207,9 +189,8 @@ fun StreamItemPreview() {
creator = "Video Creator",
thumbnail = null,
lengthInMs = 15 * 60 * 1000,
onDragStart = {},
onDragEnd = {},
onClicked = {}
onClicked = {},
reorderableScope = null
)
}
}