diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Song.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Song.kt index 514777cf..e1ae3aac 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Song.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Song.kt @@ -11,7 +11,9 @@ data class Song( val thumbnailUri: Uri? = null, val albumId: Long? = null, val artistId: Long? = null, - val isLocal: Boolean = false + val isLocal: Boolean = false, + val creationDate: Long? = null, + val dateAdded: Long? = null, ) { fun toggleLike(): Song { return copy( diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/SongDatabase.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/SongDatabase.kt index b41ac4cf..5d6bd8d1 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/SongDatabase.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/SongDatabase.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration import app.suhasdissa.vibeyou.backend.database.dao.RawDao import app.suhasdissa.vibeyou.backend.database.dao.SearchDao import app.suhasdissa.vibeyou.backend.database.dao.SongsDao @@ -15,7 +16,7 @@ import app.suhasdissa.vibeyou.backend.database.entities.SongEntity SongEntity::class, SearchQuery::class ], - version = 1, + version = 2, exportSchema = false ) abstract class SongDatabase : RoomDatabase() { @@ -28,6 +29,11 @@ abstract class SongDatabase : RoomDatabase() { @Volatile private var INSTANCE: SongDatabase? = null + private val MIGRATION_1_2 = Migration(1, 2) { database -> + database.execSQL("ALTER TABLE song ADD COLUMN creationDate INTEGER") + database.execSQL("ALTER TABLE song ADD COLUMN dateAdded INTEGER") + } + fun getDatabase(context: Context): SongDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( @@ -35,6 +41,7 @@ abstract class SongDatabase : RoomDatabase() { SongDatabase::class.java, "song_database" ) + .addMigrations(MIGRATION_1_2) .fallbackToDestructiveMigration() .allowMainThreadQueries().build() INSTANCE = instance diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt index 4c5c47ee..a8ceac0c 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/LocalMusicRepository.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Build import android.provider.MediaStore import android.text.format.DateUtils +import android.util.Log import androidx.core.net.toUri import app.suhasdissa.vibeyou.backend.data.Album import app.suhasdissa.vibeyou.backend.data.Artist @@ -44,7 +45,9 @@ class LocalMusicRepository( MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM_ID, - MediaStore.Audio.Media.ARTIST_ID + MediaStore.Audio.Media.ARTIST_ID, + MediaStore.Audio.Media.DATE_MODIFIED, + MediaStore.Audio.Media.DATE_ADDED, ) val sortOrder = "${MediaStore.Audio.Media.TITLE} ASC" @@ -66,6 +69,8 @@ class LocalMusicRepository( val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST) val albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID) val artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID) + val creationDateColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_MODIFIED) + val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATE_ADDED) while (cursor.moveToNext()) { val id = cursor.getLong(idColumn) @@ -99,7 +104,9 @@ class LocalMusicRepository( artistsText = cursor.getString(artistColumn), albumId = albumId, artistId = cursor.getLong(artistIdColumn), - isLocal = true + isLocal = true, + creationDate = cursor.getLong(creationDateColumn), + dateAdded = cursor.getLong(dateAddedColumn) ) ) } diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/LocalSongViewModel.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/LocalSongViewModel.kt index 36d44287..529233b5 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/LocalSongViewModel.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/LocalSongViewModel.kt @@ -14,6 +14,7 @@ import app.suhasdissa.vibeyou.backend.data.Album import app.suhasdissa.vibeyou.backend.data.Artist import app.suhasdissa.vibeyou.backend.data.Song import app.suhasdissa.vibeyou.backend.repository.LocalMusicRepository +import app.suhasdissa.vibeyou.ui.dialogs.SortOrder import kotlinx.coroutines.launch class LocalSongViewModel(private val musicRepository: LocalMusicRepository) : ViewModel() { @@ -21,6 +22,9 @@ class LocalSongViewModel(private val musicRepository: LocalMusicRepository) : Vi var albums by mutableStateOf(listOf()) var artists by mutableStateOf(listOf()) + var songsSortOrder = SortOrder.Alphabetic + var reverseSongs = false + init { viewModelScope.launch { try { @@ -45,6 +49,16 @@ class LocalSongViewModel(private val musicRepository: LocalMusicRepository) : Vi } } + fun updateSongsSortOrder() { + val sortedSongs = when (songsSortOrder) { + SortOrder.Alphabetic -> songs.sortedBy { it.title.lowercase() } + SortOrder.Creation_Date -> songs.sortedBy { it.creationDate } + SortOrder.Date_Added -> songs.sortedBy { it.dateAdded } + SortOrder.Artist_Name -> songs.sortedBy { it.artistsText.orEmpty().lowercase() } + } + songs = if (reverseSongs) sortedSongs.reversed() else sortedSongs + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt index 6ef18fed..a2ef8531 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlayerViewModel.kt @@ -72,10 +72,15 @@ class PlayerViewModel( controller!!.seekToPrevious() } - fun shuffleSongs(songs: List) { + fun playSongs(songs: List, shuffle: Boolean = false) { viewModelScope.launch { - val shuffleQueue = songs.shuffled().map { it.asMediaItem } - playAll(shuffleQueue) + val queue = if (shuffle) { + songs.shuffled() + } else { + songs + } + .map { it.asMediaItem } + playAll(queue) } } diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/components/ChipSelect.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/components/ChipSelect.kt index 87baefaa..6825b28f 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/components/ChipSelect.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/components/ChipSelect.kt @@ -37,7 +37,7 @@ inline fun > ChipSelector( view.playSoundEffect(SoundEffectConstants.CLICK) selectedOption = it onItemSelected(it) - }, label = { Text(it.name) }) + }, label = { Text(it.name.replace("_", " ")) }) } } } diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/dialogs/SortOrderDialog.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/dialogs/SortOrderDialog.kt new file mode 100644 index 00000000..b5cf12bd --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/dialogs/SortOrderDialog.kt @@ -0,0 +1,85 @@ +package app.suhasdissa.vibeyou.ui.dialogs + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.suhasdissa.vibeyou.R +import app.suhasdissa.vibeyou.ui.components.ChipSelector + +enum class SortOrder { + Alphabetic, + Creation_Date, + Date_Added, + Artist_Name +} + +@Composable +fun SortOrderDialog( + onDismissRequest: () -> Unit, + onSortOrderChange: (order: SortOrder, reverse: Boolean) -> Unit, + defaultSortOrder: SortOrder, + defaultReverse: Boolean, +) { + var sortOrder by remember { + mutableStateOf(defaultSortOrder) + } + var reverse by remember { + mutableStateOf(defaultReverse) + } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(stringResource(R.string.sort_order)) + }, + text = { + Column { + ChipSelector( + onItemSelected = { + sortOrder = it + }, + defaultValue = sortOrder + ) + val interactionSource = remember { MutableInteractionSource() } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(interactionSource = interactionSource, indication = null) { + reverse = !reverse + } + ) { + Checkbox(checked = reverse, onCheckedChange = { reverse = it }) + Text(text = stringResource(R.string.reversed)) + } + } + }, + confirmButton = { + TextButton( + onClick = { + onSortOrderChange(sortOrder, reverse) + onDismissRequest() + } + ) { + Text(stringResource(R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/LocalMusicScreen.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/LocalMusicScreen.kt index 0b5e4c4f..0ba1c2cb 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/LocalMusicScreen.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/LocalMusicScreen.kt @@ -2,14 +2,21 @@ package app.suhasdissa.vibeyou.ui.screens.music import android.view.SoundEffectConstants import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Shuffle +import androidx.compose.material.icons.filled.Sort import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -18,8 +25,14 @@ import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -31,6 +44,7 @@ import app.suhasdissa.vibeyou.backend.viewmodel.LocalSongViewModel import app.suhasdissa.vibeyou.backend.viewmodel.PlayerViewModel import app.suhasdissa.vibeyou.ui.components.AlbumList import app.suhasdissa.vibeyou.ui.components.ArtistList +import app.suhasdissa.vibeyou.ui.dialogs.SortOrderDialog import app.suhasdissa.vibeyou.ui.screens.songs.SongListView import kotlinx.coroutines.launch @@ -44,6 +58,11 @@ fun LocalMusicScreen( ) { val pagerState = rememberPagerState { 3 } val scope = rememberCoroutineScope() + + var showSortDialog by remember { + mutableStateOf(false) + } + Column { TabRow(selectedTabIndex = pagerState.currentPage, Modifier.fillMaxWidth()) { val view = LocalView.current @@ -98,14 +117,28 @@ fun LocalMusicScreen( 0 -> { val view = LocalView.current Scaffold(floatingActionButton = { - FloatingActionButton(onClick = { - view.playSoundEffect(SoundEffectConstants.CLICK) - playerViewModel.shuffleSongs(localSongViewModel.songs) - }) { - Icon( - imageVector = Icons.Default.Shuffle, - contentDescription = stringResource(R.string.shuffle) - ) + Row { + FloatingActionButton(onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + playerViewModel.playSongs(localSongViewModel.songs) + }) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = stringResource(R.string.play_all) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + FloatingActionButton(onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + playerViewModel.playSongs(localSongViewModel.songs, shuffle = true) + }) { + Icon( + imageVector = Icons.Default.Shuffle, + contentDescription = stringResource(R.string.shuffle) + ) + } } }) { innerPadding -> Column( @@ -113,7 +146,21 @@ fun LocalMusicScreen( .fillMaxSize() .padding(innerPadding) ) { - SongListView(songs = localSongViewModel.songs) + Row( + modifier = Modifier + .align(Alignment.End) + .padding(top = 4.dp) + .clip(RoundedCornerShape(12.dp)) + .padding(horizontal = 10.dp, vertical = 6.dp) + .clickable { + showSortDialog = true + } + ) { + Text(text = stringResource(R.string.sort_order)) + Spacer(modifier = Modifier.width(6.dp)) + Icon(imageVector = Icons.Default.Sort, contentDescription = null) + } + SongListView(songs = localSongViewModel.songs, sortOrder = localSongViewModel.songsSortOrder) } } } @@ -134,4 +181,17 @@ fun LocalMusicScreen( } } } + + if (showSortDialog) { + SortOrderDialog( + onDismissRequest = { showSortDialog = false }, + defaultSortOrder = localSongViewModel.songsSortOrder, + defaultReverse = localSongViewModel.reverseSongs, + onSortOrderChange = { sortOrder, reverse -> + localSongViewModel.songsSortOrder = sortOrder + localSongViewModel.reverseSongs = reverse + localSongViewModel.updateSongsSortOrder() + } + ) + } } diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/search/AlbumScreen.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/search/AlbumScreen.kt index 117d5ebc..ef95901f 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/search/AlbumScreen.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/search/AlbumScreen.kt @@ -50,7 +50,7 @@ fun AlbumScreen( MiniPlayerScaffold(fab = { if (state is AlbumInfoState.Success) { FloatingActionButton(onClick = { - playerViewModel.shuffleSongs(state.songs) + playerViewModel.playSongs(state.songs, shuffle = true) }) { Icon( imageVector = Icons.Default.Shuffle, diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongListView.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongListView.kt index 975f339b..c696f227 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongListView.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongListView.kt @@ -30,12 +30,15 @@ import app.suhasdissa.vibeyou.backend.viewmodel.PlayerViewModel import app.suhasdissa.vibeyou.ui.components.IllustratedMessageScreen import app.suhasdissa.vibeyou.ui.components.SongCard import app.suhasdissa.vibeyou.ui.components.SongSettingsSheet +import app.suhasdissa.vibeyou.ui.dialogs.SortOrder +import app.suhasdissa.vibeyou.utils.TimeUtil import my.nanihadesuka.compose.LazyColumnScrollbar @OptIn(ExperimentalFoundationApi::class) @Composable fun SongListView( songs: List, + sortOrder: SortOrder = SortOrder.Alphabetic, playerViewModel: PlayerViewModel = viewModel(factory = PlayerViewModel.Factory) ) { var showSongSettings by remember { mutableStateOf(false) } @@ -43,7 +46,14 @@ fun SongListView( if (songs.isEmpty()) { IllustratedMessageScreen(image = R.drawable.ic_launcher_monochrome) } else { - val groups = remember(songs) { songs.groupBy { song -> song.title.first().toString() } } + val groups = remember(songs) { + when (sortOrder) { + SortOrder.Alphabetic -> songs.groupBy { song -> song.title.first().toString() } + SortOrder.Artist_Name -> songs.groupBy { song -> song.artistsText.orEmpty() } + SortOrder.Creation_Date -> songs.groupBy { song -> TimeUtil.getYear(song.creationDate ?: 0).toString() } + SortOrder.Date_Added -> songs.groupBy { song -> TimeUtil.getYear(song.dateAdded ?: 0).toString() } + } + } val state = rememberLazyListState() LazyColumnScrollbar( listState = state, diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongsScreen.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongsScreen.kt index ec36bafa..42d23d21 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongsScreen.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/songs/SongsScreen.kt @@ -34,7 +34,7 @@ fun SongsScreen( Scaffold(floatingActionButton = { FloatingActionButton(onClick = { view.playSoundEffect(SoundEffectConstants.CLICK) - playerViewModel.shuffleSongs(songs) + playerViewModel.playSongs(songs) }) { Icon( imageVector = Icons.Default.Shuffle, diff --git a/app/src/main/java/app/suhasdissa/vibeyou/utils/TimeUtil.kt b/app/src/main/java/app/suhasdissa/vibeyou/utils/TimeUtil.kt new file mode 100644 index 00000000..adf1ffc9 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/utils/TimeUtil.kt @@ -0,0 +1,11 @@ +package app.suhasdissa.vibeyou.utils + +import java.time.Instant +import java.time.ZoneId + +object TimeUtil { + fun getYear(timestamp: Long): Int { + val instant = Instant.ofEpochSecond(timestamp) + return instant.atZone(ZoneId.systemDefault()).year + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00fede2e..c8aa7eeb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,4 +64,8 @@ Unlimited Playback speed Pitch + Play all + Sort Order + Cancel + Reversed \ No newline at end of file