Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ data class Video(
val duration: Long,
val uriString: String,
val displayName: String,
var isSelected: Boolean = false,
val nameWithExtension: String,
val width: Int,
val height: Int,
Expand All @@ -31,6 +32,7 @@ data class Video(
uriString = "",
nameWithExtension = "Avengers Endgame (2019) BluRay x264.mp4",
duration = 1000,
isSelected = false,
displayName = "Avengers Endgame (2019) BluRay x264",
width = 1920,
height = 1080,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material.icons.rounded.Replay10
import androidx.compose.material.icons.rounded.ResetTv
import androidx.compose.material.icons.rounded.ScreenRotationAlt
import androidx.compose.material.icons.rounded.SelectAll
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material.icons.rounded.Speed
Expand Down Expand Up @@ -93,6 +94,7 @@ object NextIcons {
val Link = Icons.Rounded.Link
val Location = Icons.Rounded.LocationOn
val Movie = Icons.Rounded.LocalMovies
val MultiSelect = Icons.Rounded.SelectAll
val PhotoSize = Icons.Rounded.PhotoSizeSelectSmall
val Pinch = Icons.Rounded.Pinch
val Player = Icons.Rounded.PlayCircle
Expand Down
2 changes: 2 additions & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@
<string name="frame_rate">Frame rate</string>
<string name="okay">Okay</string>
<string name="properties">Properties</string>
<string name="multi_select">Multi-select</string>
<string name="selected_tracks_count">%1$d/%2$d Selected</string>
<string name="volume_boost">Volume boost</string>
<string name="volume_boost_desc">Boost audio volume up to 200%</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ class MediaPickerScreenTest {
foldersState = FoldersState.Loading,
preferences = ApplicationPreferences(),
onDeleteVideoClick = {},
onDeleteFolderClick = {}
onDeleteFolderClick = {},
selectedTracks = emptyList(),
clearSelectedTracks = {}
)
}
}
Expand All @@ -59,7 +61,9 @@ class MediaPickerScreenTest {
foldersState = FoldersState.Loading,
preferences = ApplicationPreferences().copy(groupVideosByFolder = false),
onDeleteVideoClick = {},
onDeleteFolderClick = {}
onDeleteFolderClick = {},
selectedTracks = videoItemsTestData,
clearSelectedTracks = {}
)
}
}
Expand Down Expand Up @@ -98,7 +102,9 @@ class MediaPickerScreenTest {
),
preferences = ApplicationPreferences().copy(groupVideosByFolder = true),
onDeleteVideoClick = {},
onDeleteFolderClick = {}
onDeleteFolderClick = {},
selectedTracks = emptyList(),
clearSelectedTracks = {}
)
}
}
Expand Down Expand Up @@ -137,7 +143,9 @@ class MediaPickerScreenTest {
foldersState = FoldersState.Loading,
preferences = ApplicationPreferences().copy(groupVideosByFolder = false),
onDeleteVideoClick = {},
onDeleteFolderClick = {}
onDeleteFolderClick = {},
selectedTracks = emptyList(),
clearSelectedTracks = {}
)
}
}
Expand Down Expand Up @@ -166,7 +174,9 @@ class MediaPickerScreenTest {
),
preferences = ApplicationPreferences().copy(groupVideosByFolder = true),
onDeleteVideoClick = {},
onDeleteFolderClick = {}
onDeleteFolderClick = {},
selectedTracks = emptyList(),
clearSelectedTracks = {}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.anilbeesetti.nextplayer.feature.videopicker.composables

import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
Expand All @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
Expand Down Expand Up @@ -41,10 +43,19 @@ import dev.anilbeesetti.nextplayer.core.ui.theme.NextPlayerTheme
fun VideoItem(
video: Video,
preferences: ApplicationPreferences,
isSelected: Boolean,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val selectedItemColor = if (!isSystemInDarkTheme()) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
} else {
MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f)
}
ListItem(
colors = ListItemDefaults.colors(
containerColor = if (isSelected) selectedItemColor else ListItemDefaults.containerColor
),
leadingContent = {
Box(
modifier = Modifier
Expand All @@ -53,14 +64,6 @@ fun VideoItem(
.width(min(150.dp, LocalConfiguration.current.screenWidthDp.dp * 0.35f))
.aspectRatio(16f / 10f)
) {
Icon(
imageVector = NextIcons.Video,
contentDescription = null,
tint = MaterialTheme.colorScheme.surfaceColorAtElevation(100.dp),
modifier = Modifier
.align(Alignment.Center)
.fillMaxSize(0.5f)
)
if (preferences.showThumbnailField) {
AsyncImage(
model = ImageRequest.Builder(context)
Expand All @@ -70,7 +73,17 @@ fun VideoItem(
contentDescription = null,
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
alpha = if (isSelected) 0.2f else 1f
)
} else {
Icon(
imageVector = NextIcons.Video,
contentDescription = null,
tint = MaterialTheme.colorScheme.surfaceColorAtElevation(100.dp),
modifier = Modifier
.align(Alignment.Center)
.fillMaxSize(0.5f)
)
}
if (preferences.showDurationField) {
Expand All @@ -84,6 +97,14 @@ fun VideoItem(
shape = MaterialTheme.shapes.extraSmall
)
}
if (isSelected) {
Icon(
imageVector = NextIcons.CheckBox,
contentDescription = "Selected",
modifier = Modifier
.align(Alignment.BottomStart)
)
}
}
},
headlineContent = {
Expand Down Expand Up @@ -129,7 +150,7 @@ fun VideoItem(
fun VideoItemPreview() {
NextPlayerTheme {
Surface {
VideoItem(video = Video.sample, preferences = ApplicationPreferences())
VideoItem(video = Video.sample, isSelected = true, preferences = ApplicationPreferences())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import dev.anilbeesetti.nextplayer.core.ui.R
import dev.anilbeesetti.nextplayer.core.ui.components.NextDialog
import dev.anilbeesetti.nextplayer.core.ui.designsystem.NextIcons
import dev.anilbeesetti.nextplayer.feature.videopicker.screens.VideosState
import dev.anilbeesetti.nextplayer.feature.videopicker.screens.media.MediaPickerViewModel
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
Expand All @@ -47,13 +48,22 @@ fun VideosView(
videosState: VideosState,
preferences: ApplicationPreferences,
onVideoClick: (Uri) -> Unit,
onDeleteVideoClick: (String) -> Unit,
onVideoLoaded: (Uri) -> Unit = {}
onDeleteVideoClick: (List<String>) -> Unit,
toggleMultiSelect: () -> Unit,
disableMultiSelect: Boolean,
totalVideos: (Int) -> Unit,
onVideoLoaded: (Uri) -> Unit = {},
viewModel: MediaPickerViewModel?
) {
val haptic = LocalHapticFeedback.current
var showMediaActionsFor: Video? by rememberSaveable { mutableStateOf(null) }
var deleteAction: Video? by rememberSaveable { mutableStateOf(null) }
var showInfoAction: Video? by rememberSaveable { mutableStateOf(null) }
var multiSelect by rememberSaveable { mutableStateOf(false) }

if (disableMultiSelect) {
multiSelect = false
}
val scope = rememberCoroutineScope()
val context = LocalContext.current
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
Expand All @@ -63,19 +73,36 @@ fun VideosView(
is VideosState.Success -> if (videosState.data.isEmpty()) {
NoVideosFound()
} else {
totalVideos(videosState.data.size)
MediaLazyList {
items(videosState.data, key = { it.path }) { video ->
if (disableMultiSelect) {
viewModel?.videoTracks = videosState.data.map { it.copy() }
}

items(viewModel?.videoTracks ?: videosState.data, key = { it.path }) { video ->
LaunchedEffect(Unit) {
onVideoLoaded(Uri.parse(video.uriString))
}
VideoItem(
video = video,
preferences = preferences,
isSelected = video.isSelected,
modifier = Modifier.combinedClickable(
onClick = { onVideoClick(Uri.parse(video.uriString)) },
onClick = {
if (multiSelect) {
video.isSelected = !video.isSelected
viewModel?.let {
toggleSelection(video, it)
}
} else {
onVideoClick(Uri.parse(video.uriString))
}
},
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
showMediaActionsFor = video
if (!multiSelect) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
showMediaActionsFor = video
}
}
)
)
Expand Down Expand Up @@ -128,6 +155,17 @@ fun VideosView(
}
}
)
BottomSheetItem(
text = stringResource(R.string.multi_select),
icon = NextIcons.MultiSelect,
onClick = {
multiSelect = true
toggleMultiSelect()
scope.launch { bottomSheetState.hide() }.invokeOnCompletion {
if (!bottomSheetState.isVisible) showMediaActionsFor = null
}
}
)
}
}

Expand All @@ -136,7 +174,7 @@ fun VideosView(
subText = stringResource(id = R.string.delete_file),
onCancel = { deleteAction = null },
onConfirm = {
onDeleteVideoClick(it.uriString)
onDeleteVideoClick(listOf(it.uriString))
deleteAction = null
},
fileNames = listOf(it.nameWithExtension)
Expand All @@ -151,6 +189,14 @@ fun VideosView(
}
}

private fun toggleSelection(video: Video, viewModel: MediaPickerViewModel) {
if (video.isSelected) {
viewModel.addToSelectedTracks(video)
} else {
viewModel.removeFromSelectedTracks(video)
}
}

@Composable
fun ShowVideoInfoDialog(
video: Video,
Expand Down
Loading