diff --git a/core/model/src/main/java/dev/anilbeesetti/nextplayer/core/model/ApplicationPreferences.kt b/core/model/src/main/java/dev/anilbeesetti/nextplayer/core/model/ApplicationPreferences.kt index 30486caf2..5e8d9315f 100644 --- a/core/model/src/main/java/dev/anilbeesetti/nextplayer/core/model/ApplicationPreferences.kt +++ b/core/model/src/main/java/dev/anilbeesetti/nextplayer/core/model/ApplicationPreferences.kt @@ -9,5 +9,6 @@ data class ApplicationPreferences( val groupVideosByFolder: Boolean = true, val themeConfig: ThemeConfig = ThemeConfig.SYSTEM, val useDynamicColors: Boolean = true, - val excludeFolders: List = emptyList() + val excludeFolders: List = emptyList(), + val isShuffleOn: Boolean = false ) diff --git a/core/ui/src/main/java/dev/anilbeesetti/nextplayer/core/ui/designsystem/NextIcons.kt b/core/ui/src/main/java/dev/anilbeesetti/nextplayer/core/ui/designsystem/NextIcons.kt index 0059499be..57affc2a2 100644 --- a/core/ui/src/main/java/dev/anilbeesetti/nextplayer/core/ui/designsystem/NextIcons.kt +++ b/core/ui/src/main/java/dev/anilbeesetti/nextplayer/core/ui/designsystem/NextIcons.kt @@ -37,6 +37,8 @@ import androidx.compose.material.icons.rounded.ResetTv import androidx.compose.material.icons.rounded.ScreenRotationAlt import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Share +import androidx.compose.material.icons.rounded.Shuffle +import androidx.compose.material.icons.rounded.ShuffleOn import androidx.compose.material.icons.rounded.Speed import androidx.compose.material.icons.rounded.Straighten import androidx.compose.material.icons.rounded.Style @@ -91,6 +93,8 @@ object NextIcons { val Style = Icons.Rounded.Style val Subtitle = Icons.Rounded.Subtitles val Size = Icons.Rounded.CompareArrows + val Shuffle = Icons.Rounded.Shuffle + val ShuffleOn = Icons.Rounded.ShuffleOn val Speed = Icons.Rounded.Speed val SwipeHorizontal = Icons.Rounded.Swipe val SwipeVertical = Icons.Rounded.SwipeVertical diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index c2b05705f..134ce7258 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -140,4 +140,6 @@ Delete Delete the following file Subtitle text encoding + Shuffle enabled + Shuffle disabled \ No newline at end of file diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt index bf83448c2..69c734421 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerActivity.kt @@ -71,6 +71,7 @@ import dev.anilbeesetti.nextplayer.feature.player.utils.PlaylistManager import dev.anilbeesetti.nextplayer.feature.player.utils.toMillis import java.nio.charset.Charset import java.util.Arrays +import java.util.Collections import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -372,7 +373,11 @@ class PlayerActivity : AppCompatActivity() { if (mediaUri != null) { launch(Dispatchers.IO) { - val playlist = viewModel.getPlaylistFromUri(mediaUri) + var playlist = viewModel.getPlaylistFromUri(mediaUri) + if (applicationPreferences.isShuffleOn) { + playlist = viewModel.getShuffledPlaylist(mediaUri) + Collections.swap(playlist, 0, playlist.indexOf(mediaUri)) + } playlistManager.setPlaylist(playlist) } } diff --git a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt index 17db3857e..01d99f081 100644 --- a/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt +++ b/feature/player/src/main/java/dev/anilbeesetti/nextplayer/feature/player/PlayerViewModel.kt @@ -66,6 +66,10 @@ class PlayerViewModel @Inject constructor( return getSortedPlaylistUseCase.invoke(uri) } + suspend fun getShuffledPlaylist(uri: Uri): List { + return getPlaylistFromUri(uri).shuffled() + } + fun saveState( path: String?, position: Long, diff --git a/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderScreen.kt b/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderScreen.kt index f0ffe9516..26d6f9971 100644 --- a/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderScreen.kt +++ b/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderScreen.kt @@ -1,6 +1,7 @@ package dev.anilbeesetti.nextplayer.feature.videopicker.screens.mediaFolder import android.net.Uri +import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box @@ -9,10 +10,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle @@ -45,7 +48,8 @@ fun MediaPickerFolderRoute( videosState = videosState, onVideoClick = onVideoClick, onNavigateUp = onNavigateUp, - onDeleteVideoClick = { viewModel.deleteVideos(listOf(it), deleteIntentSenderLauncher) } + onDeleteVideoClick = { viewModel.deleteVideos(listOf(it), deleteIntentSenderLauncher) }, + viewModel = viewModel ) } @@ -56,23 +60,60 @@ internal fun MediaPickerFolderScreen( videosState: VideosState, onNavigateUp: () -> Unit, onVideoClick: (Uri) -> Unit, - onDeleteVideoClick: (String) -> Unit + onDeleteVideoClick: (String) -> Unit, + viewModel: MediaPickerFolderViewModel ) { + val prefs = viewModel.appPrefs.collectAsStateWithLifecycle() + val context = LocalContext.current Column { - NextTopAppBar( - title = File(folderPath).prettyName, - navigationIcon = { - IconButton(onClick = onNavigateUp) { - Icon( - imageVector = NextIcons.ArrowBack, - contentDescription = stringResource(id = R.string.navigate_up) - ) - } + NextTopAppBar(title = File(folderPath).prettyName, navigationIcon = { + IconButton(onClick = onNavigateUp) { + Icon( + imageVector = NextIcons.ArrowBack, + contentDescription = stringResource(id = R.string.navigate_up) + ) } - ) + }, actions = { + IconButton(onClick = { + if (prefs.value.isShuffleOn) { + Toast.makeText( + context, + R.string.shuffle_disabled, + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText(context, R.string.shuffle_enabled, Toast.LENGTH_SHORT).show() + when (videosState) { + is VideosState.Success -> { + onVideoClick( + Uri.parse( + videosState.data.shuffled().first().uriString + ) + ) + } + + else -> {} + } + } + viewModel.toggleShuffle() + }) { + if (prefs.value.isShuffleOn) { + Icon( + imageVector = NextIcons.ShuffleOn, + contentDescription = stringResource(id = R.string.shuffle_enabled), + tint = MaterialTheme.colorScheme.primary + ) + } else { + Icon( + imageVector = NextIcons.Shuffle, + contentDescription = stringResource(id = R.string.shuffle_enabled), + tint = MaterialTheme.colorScheme.secondary + ) + } + } + }) Box( - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { VideosListFromState(videosState = videosState, onVideoClick = onVideoClick, onDeleteVideoClick = onDeleteVideoClick) diff --git a/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderViewModel.kt b/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderViewModel.kt index e66522a67..84efa78cf 100644 --- a/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderViewModel.kt +++ b/feature/videopicker/src/main/java/dev/anilbeesetti/nextplayer/feature/videopicker/screens/mediaFolder/MediaPickerFolderViewModel.kt @@ -7,7 +7,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.anilbeesetti.nextplayer.core.data.repository.MediaRepository +import dev.anilbeesetti.nextplayer.core.data.repository.PreferencesRepository import dev.anilbeesetti.nextplayer.core.domain.GetSortedVideosUseCase +import dev.anilbeesetti.nextplayer.core.model.ApplicationPreferences import dev.anilbeesetti.nextplayer.feature.videopicker.navigation.FolderArgs import dev.anilbeesetti.nextplayer.feature.videopicker.screens.VideosState import javax.inject.Inject @@ -20,13 +22,20 @@ import kotlinx.coroutines.launch class MediaPickerFolderViewModel @Inject constructor( getSortedVideosUseCase: GetSortedVideosUseCase, savedStateHandle: SavedStateHandle, - private val mediaRepository: MediaRepository + private val mediaRepository: MediaRepository, + private val preferencesRepository: PreferencesRepository ) : ViewModel() { private val folderArgs = FolderArgs(savedStateHandle) val folderPath = folderArgs.folderId + val appPrefs = preferencesRepository.applicationPreferences.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = ApplicationPreferences() + ) + val videos = getSortedVideosUseCase.invoke(folderPath) .map { VideosState.Success(it) } .stateIn( @@ -40,4 +49,14 @@ class MediaPickerFolderViewModel @Inject constructor( mediaRepository.deleteVideos(uris, intentSenderLauncher) } } + + fun toggleShuffle() { + viewModelScope.launch { + preferencesRepository.updateApplicationPreferences { + it.copy( + isShuffleOn = !it.isShuffleOn + ) + } + } + } }