From a6633cd80c721d1d68f7787fbc7ececd8afe5c11 Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Tue, 12 Dec 2023 14:35:27 +0530 Subject: [PATCH] Playlist Support (#84) --- app/build.gradle.kts | 4 + .../1.json | 106 ++++++++ .../2.json | 226 ++++++++++++++++++ .../app/suhasdissa/vibeyou/AppContainer.kt | 5 + .../app/suhasdissa/vibeyou/Destination.kt | 1 + .../java/app/suhasdissa/vibeyou/NavHost.kt | 9 + .../suhasdissa/vibeyou/backend/data/Album.kt | 9 +- .../vibeyou/backend/database/SongDatabase.kt | 16 +- .../backend/database/dao/PlaylistDao.kt | 31 +++ .../database/entities/PlaylistEntity.kt | 14 ++ .../database/entities/PlaylistWithSongs.kt | 20 ++ .../backend/database/entities/SongEntity.kt | 4 +- .../database/entities/SongPlaylistMap.kt | 28 +++ .../backend/repository/PlaylistRepository.kt | 39 +++ .../backend/viewmodel/PlaylistViewModel.kt | 66 +++++ .../vibeyou/ui/screens/home/HomeScreen.kt | 11 +- .../vibeyou/ui/screens/music/MusicScreen.kt | 33 ++- .../vibeyou/ui/screens/search/AlbumScreen.kt | 9 +- .../app/suhasdissa/vibeyou/utils/Mappers.kt | 18 ++ app/src/main/res/values/strings.xml | 3 +- 20 files changed, 638 insertions(+), 14 deletions(-) create mode 100644 app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/1.json create mode 100644 app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/2.json create mode 100644 app/src/main/java/app/suhasdissa/vibeyou/backend/database/dao/PlaylistDao.kt create mode 100644 app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistEntity.kt create mode 100644 app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistWithSongs.kt create mode 100644 app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongPlaylistMap.kt create mode 100644 app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PlaylistRepository.kt create mode 100644 app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlaylistViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6031951e..e3531850 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,6 +16,10 @@ android { versionCode = 2 versionName = "2.0" + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/1.json b/app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/1.json new file mode 100644 index 00000000..ca76dac2 --- /dev/null +++ b/app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/1.json @@ -0,0 +1,106 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "ada7c6c2bcd1884e04cc9a6260733689", + "entities": [ + { + "tableName": "song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `thumbnailUrl` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ada7c6c2bcd1884e04cc9a6260733689')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/2.json b/app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/2.json new file mode 100644 index 00000000..251a35aa --- /dev/null +++ b/app/schemas/app.suhasdissa.vibeyou.backend.database.SongDatabase/2.json @@ -0,0 +1,226 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "14bb9b7b114b881a7c2b94d9f2675cd3", + "entities": [ + { + "tableName": "song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `thumbnailUrl` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLocal", + "columnName": "isLocal", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `type` TEXT NOT NULL, `subTitle` TEXT, `thumbnailUrl` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subTitle", + "columnName": "subTitle", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "playlist_songs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`playlistId`, `songId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `playlists`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "playlistId", + "songId" + ] + }, + "indices": [ + { + "name": "index_playlist_songs_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + }, + { + "name": "index_playlist_songs_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_songId` ON `${TABLE_NAME}` (`songId`)" + } + ], + "foreignKeys": [ + { + "table": "song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14bb9b7b114b881a7c2b94d9f2675cd3')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/app/suhasdissa/vibeyou/AppContainer.kt b/app/src/main/java/app/suhasdissa/vibeyou/AppContainer.kt index ed32b5d2..ce1b1c92 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/AppContainer.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/AppContainer.kt @@ -7,6 +7,7 @@ import app.suhasdissa.vibeyou.backend.repository.AuthRepository import app.suhasdissa.vibeyou.backend.repository.AuthRepositoryImpl import app.suhasdissa.vibeyou.backend.repository.LocalMusicRepository import app.suhasdissa.vibeyou.backend.repository.PipedMusicRepository +import app.suhasdissa.vibeyou.backend.repository.PlaylistRepository import app.suhasdissa.vibeyou.backend.repository.SongDatabaseRepository import app.suhasdissa.vibeyou.backend.repository.SongDatabaseRepositoryImpl import com.google.common.util.concurrent.ListenableFuture @@ -16,6 +17,7 @@ interface AppContainer { val songDatabaseRepository: SongDatabaseRepository val pipedMusicRepository: PipedMusicRepository val localMusicRepository: LocalMusicRepository + val playlistRepository: PlaylistRepository val authRepository: AuthRepository val controllerFuture: ListenableFuture val contentResolver: ContentResolver @@ -35,6 +37,9 @@ class DefaultAppContainer( override val localMusicRepository: LocalMusicRepository by lazy { LocalMusicRepository(contentResolver, database.searchDao()) } + override val playlistRepository: PlaylistRepository by lazy { + PlaylistRepository(database.playlistDao(), database.songsDao()) + } override val authRepository: AuthRepository by lazy { AuthRepositoryImpl() } diff --git a/app/src/main/java/app/suhasdissa/vibeyou/Destination.kt b/app/src/main/java/app/suhasdissa/vibeyou/Destination.kt index bb5bc79b..4d19ea8e 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/Destination.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/Destination.kt @@ -12,6 +12,7 @@ sealed class Destination(val route: String) { object AppearanceSettings : Destination("appearance_settings") object Playlists : Destination("playlist_screen") object LocalPlaylists : Destination("local_playlist_screen") + object SavedPlaylists : Destination("saved_playlist_screen") object Artist : Destination("artist") object LocalArtist : Destination("local_artist") } diff --git a/app/src/main/java/app/suhasdissa/vibeyou/NavHost.kt b/app/src/main/java/app/suhasdissa/vibeyou/NavHost.kt index e7b62fd9..5ddcfe5b 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/NavHost.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/NavHost.kt @@ -10,6 +10,7 @@ import androidx.navigation.compose.composable import app.suhasdissa.vibeyou.backend.viewmodel.LocalSearchViewModel import app.suhasdissa.vibeyou.backend.viewmodel.LocalSongViewModel import app.suhasdissa.vibeyou.backend.viewmodel.PipedSearchViewModel +import app.suhasdissa.vibeyou.backend.viewmodel.PlaylistViewModel import app.suhasdissa.vibeyou.ui.screens.home.HomeScreen import app.suhasdissa.vibeyou.ui.screens.search.AlbumScreen import app.suhasdissa.vibeyou.ui.screens.search.ArtistScreen @@ -93,6 +94,14 @@ fun AppNavHost(navHostController: NavHostController) { } } + composable(Destination.SavedPlaylists.route) { + CompositionLocalProvider(LocalViewModelStoreOwner provides viewModelStoreOwner) { + val searchViewModel: PlaylistViewModel = + viewModel(factory = PlaylistViewModel.Factory) + AlbumScreen(searchViewModel.albumInfoState) + } + } + composable(route = Destination.Artist.route) { CompositionLocalProvider(LocalViewModelStoreOwner provides viewModelStoreOwner) { val searchViewModel: PipedSearchViewModel = diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Album.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Album.kt index fe99dd97..08366798 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Album.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/data/Album.kt @@ -8,5 +8,10 @@ data class Album( val thumbnailUri: Uri? = null, val artistsText: String, val numberOfSongs: Int? = null, - val isLocal: Boolean = false -) + val isLocal: Boolean = false, + val type: Type = Type.ALBUM +) { + enum class Type { + PLAYLIST, ALBUM + } +} 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..701809de 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 @@ -1,26 +1,36 @@ package app.suhasdissa.vibeyou.backend.database import android.content.Context +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import app.suhasdissa.vibeyou.backend.database.dao.PlaylistDao import app.suhasdissa.vibeyou.backend.database.dao.RawDao import app.suhasdissa.vibeyou.backend.database.dao.SearchDao import app.suhasdissa.vibeyou.backend.database.dao.SongsDao +import app.suhasdissa.vibeyou.backend.database.entities.PlaylistEntity import app.suhasdissa.vibeyou.backend.database.entities.SearchQuery import app.suhasdissa.vibeyou.backend.database.entities.SongEntity +import app.suhasdissa.vibeyou.backend.database.entities.SongPlaylistMap @Database( entities = [ SongEntity::class, - SearchQuery::class + SearchQuery::class, + PlaylistEntity::class, + SongPlaylistMap::class ], - version = 1, - exportSchema = false + version = 2, + exportSchema = true, + autoMigrations = [ + AutoMigration(from = 1, to = 2) + ] ) abstract class SongDatabase : RoomDatabase() { abstract fun songsDao(): SongsDao + abstract fun playlistDao(): PlaylistDao abstract fun searchDao(): SearchDao abstract fun rawDao(): RawDao diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/dao/PlaylistDao.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/dao/PlaylistDao.kt new file mode 100644 index 00000000..2e1fd905 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/dao/PlaylistDao.kt @@ -0,0 +1,31 @@ +package app.suhasdissa.vibeyou.backend.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import app.suhasdissa.vibeyou.backend.database.entities.PlaylistEntity +import app.suhasdissa.vibeyou.backend.database.entities.PlaylistWithSongs +import app.suhasdissa.vibeyou.backend.database.entities.SongPlaylistMap +import kotlinx.coroutines.flow.Flow + +@Dao +interface PlaylistDao { + @Insert(entity = PlaylistEntity::class, onConflict = OnConflictStrategy.REPLACE) + fun addPlaylist(playlist: PlaylistEntity) + + @Insert(entity = SongPlaylistMap::class, onConflict = OnConflictStrategy.REPLACE) + fun addPlaylistMaps(maps: List) + + @Query("SELECT * from playlists") + fun getAllPlaylists(): Flow> + + @Transaction + @Query("SELECT * from playlists WHERE id=:id") + fun getPlaylist(id: String): PlaylistWithSongs + + @Delete(entity = PlaylistEntity::class) + fun removePlaylist(playlist: PlaylistEntity) +} diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistEntity.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistEntity.kt new file mode 100644 index 00000000..f90216f7 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistEntity.kt @@ -0,0 +1,14 @@ +package app.suhasdissa.vibeyou.backend.database.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import app.suhasdissa.vibeyou.backend.data.Album + +@Entity(tableName = "playlists") +data class PlaylistEntity( + @PrimaryKey val id: String, + val title: String, + val type: Album.Type, + val subTitle: String? = null, + val thumbnailUrl: String? = null +) diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistWithSongs.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistWithSongs.kt new file mode 100644 index 00000000..5dca8d5c --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/PlaylistWithSongs.kt @@ -0,0 +1,20 @@ +package app.suhasdissa.vibeyou.backend.database.entities + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +data class PlaylistWithSongs( + @Embedded val playlist: PlaylistEntity, + @Relation( + entity = SongEntity::class, + parentColumn = "id", + entityColumn = "id", + associateBy = Junction( + value = SongPlaylistMap::class, + parentColumn = "playlistId", + entityColumn = "songId" + ) + ) + val songs: List = listOf() +) diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongEntity.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongEntity.kt index 4782b512..1d0d2c77 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongEntity.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongEntity.kt @@ -1,5 +1,6 @@ package app.suhasdissa.vibeyou.backend.database.entities +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @@ -11,5 +12,6 @@ data class SongEntity( val durationText: String?, val thumbnailUrl: String?, val likedAt: Long? = null, - val totalPlayTimeMs: Long = 0 + val totalPlayTimeMs: Long = 0, + @ColumnInfo(defaultValue = "0") val isLocal: Boolean = false ) diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongPlaylistMap.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongPlaylistMap.kt new file mode 100644 index 00000000..50bee3e2 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/database/entities/SongPlaylistMap.kt @@ -0,0 +1,28 @@ +package app.suhasdissa.vibeyou.backend.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + tableName = "playlist_songs", + primaryKeys = ["playlistId", "songId"], + foreignKeys = [ + ForeignKey( + entity = SongEntity::class, + parentColumns = ["id"], + childColumns = ["songId"], + onDelete = ForeignKey.CASCADE + ), + ForeignKey( + entity = PlaylistEntity::class, + parentColumns = ["id"], + childColumns = ["playlistId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class SongPlaylistMap( + @ColumnInfo(index = true) val playlistId: String, + @ColumnInfo(index = true) val songId: String +) diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PlaylistRepository.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PlaylistRepository.kt new file mode 100644 index 00000000..fbae2429 --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/repository/PlaylistRepository.kt @@ -0,0 +1,39 @@ +package app.suhasdissa.vibeyou.backend.repository + +import app.suhasdissa.vibeyou.backend.data.Album +import app.suhasdissa.vibeyou.backend.data.Song +import app.suhasdissa.vibeyou.backend.database.dao.PlaylistDao +import app.suhasdissa.vibeyou.backend.database.dao.SongsDao +import app.suhasdissa.vibeyou.backend.database.entities.PlaylistEntity +import app.suhasdissa.vibeyou.backend.database.entities.PlaylistWithSongs +import app.suhasdissa.vibeyou.backend.database.entities.SongPlaylistMap +import app.suhasdissa.vibeyou.utils.asPlaylistEntity +import app.suhasdissa.vibeyou.utils.asSongEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +class PlaylistRepository(private val playlistDao: PlaylistDao, private val songsDao: SongsDao) { + private fun createNew(album: Album) { + playlistDao.addPlaylist( + album.asPlaylistEntity + ) + } + + private fun addSongsToPlaylist(playlistId: String, songs: List) { + songsDao.addSongs(songs.map { it.asSongEntity }) + val songMap = songs.map { SongPlaylistMap(playlistId, it.id) } + playlistDao.addPlaylistMaps(songMap) + } + + suspend fun newPlaylistWithSongs(album: Album, songs: List) = + withContext(Dispatchers.IO) { + createNew(album) + addSongsToPlaylist(album.id, songs) + } + + fun getPlaylists(): Flow> = playlistDao.getAllPlaylists() + + suspend fun getPlaylist(id: String): PlaylistWithSongs = + withContext(Dispatchers.IO) { return@withContext playlistDao.getPlaylist(id) } +} diff --git a/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlaylistViewModel.kt b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlaylistViewModel.kt new file mode 100644 index 00000000..c62c0f9f --- /dev/null +++ b/app/src/main/java/app/suhasdissa/vibeyou/backend/viewmodel/PlaylistViewModel.kt @@ -0,0 +1,66 @@ +package app.suhasdissa.vibeyou.backend.viewmodel + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import app.suhasdissa.vibeyou.MellowMusicApplication +import app.suhasdissa.vibeyou.backend.data.Album +import app.suhasdissa.vibeyou.backend.data.Song +import app.suhasdissa.vibeyou.backend.repository.PlaylistRepository +import app.suhasdissa.vibeyou.backend.viewmodel.state.AlbumInfoState +import app.suhasdissa.vibeyou.utils.asSong +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class PlaylistViewModel(private val playlistRepository: PlaylistRepository) : ViewModel() { + var albumInfoState: AlbumInfoState by mutableStateOf(AlbumInfoState.Loading) + private set + + var albums = playlistRepository.getPlaylists().stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000L), + initialValue = listOf() + ) + + fun getPlaylistInfo(playlist: Album) { + viewModelScope.launch { + albumInfoState = AlbumInfoState.Loading + albumInfoState = try { + Log.e("PlaylistViewModel", "Getting info") + val info = playlistRepository.getPlaylist(playlist.id) + AlbumInfoState.Success( + playlist, + info.songs.map { it.asSong } + ) + } catch (e: Exception) { + Log.e("Playlist Info", e.toString()) + AlbumInfoState.Error + } + } + } + + fun newPlaylistWithSongs(album: Album, songs: List) { + viewModelScope.launch { + playlistRepository.newPlaylistWithSongs(album, songs) + } + } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = + (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MellowMusicApplication) + PlaylistViewModel( + application.container.playlistRepository + ) + } + } + } +} diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/home/HomeScreen.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/home/HomeScreen.kt index 89caebdf..ccdb7b5e 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/home/HomeScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -38,6 +39,7 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -137,6 +139,7 @@ fun HomeScreen( } }) }) { + val viewModelStoreOwner = LocalViewModelStoreOwner.current!! NavHost( navController, startDestination = Destination.LocalMusic.route, @@ -146,9 +149,11 @@ fun HomeScreen( exitTransition = { ExitTransition.None } ) { composable(Destination.PipedMusic.route) { - MusicScreen() - LaunchedEffect(Unit) { - currentDestination = Destination.PipedMusic + CompositionLocalProvider(LocalViewModelStoreOwner provides viewModelStoreOwner) { + MusicScreen(onNavigate) + LaunchedEffect(Unit) { + currentDestination = Destination.PipedMusic + } } } composable(Destination.LocalMusic.route) { diff --git a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/MusicScreen.kt b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/MusicScreen.kt index 926c19e2..96bf59a8 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/MusicScreen.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/ui/screens/music/MusicScreen.kt @@ -13,19 +13,29 @@ import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import app.suhasdissa.vibeyou.Destination import app.suhasdissa.vibeyou.R +import app.suhasdissa.vibeyou.backend.viewmodel.PlaylistViewModel +import app.suhasdissa.vibeyou.ui.components.AlbumList import app.suhasdissa.vibeyou.ui.screens.songs.SongsScreen +import app.suhasdissa.vibeyou.utils.asAlbum import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun MusicScreen() { - val pagerState = rememberPagerState { 2 } +fun MusicScreen( + onNavigate: (Destination) -> Unit, + playlistViewModel: PlaylistViewModel = viewModel(factory = PlaylistViewModel.Factory) +) { + val pagerState = rememberPagerState { 3 } val scope = rememberCoroutineScope() Column { TabRow(selectedTabIndex = pagerState.currentPage, Modifier.fillMaxWidth()) { @@ -58,14 +68,33 @@ fun MusicScreen() { style = MaterialTheme.typography.titleMedium ) } + Tab(selected = (pagerState.currentPage == 2), onClick = { + view.playSoundEffect(SoundEffectConstants.CLICK) + scope.launch { + pagerState.animateScrollToPage( + 2 + ) + } + }) { + Text( + stringResource(R.string.playlists), + Modifier.padding(10.dp), + style = MaterialTheme.typography.titleMedium + ) + } } HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize() ) { index -> + val albums by playlistViewModel.albums.collectAsState() when (index) { 0 -> SongsScreen(showFavourites = false) 1 -> SongsScreen(showFavourites = true) + 2 -> AlbumList(items = albums.map { it.asAlbum }, onClickCard = { + playlistViewModel.getPlaylistInfo(it) + onNavigate(Destination.SavedPlaylists) + }, onLongPress = {}) } } } 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 10a5b416..febd16e6 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 @@ -40,6 +40,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import app.suhasdissa.vibeyou.R import app.suhasdissa.vibeyou.backend.data.Song import app.suhasdissa.vibeyou.backend.viewmodel.PlayerViewModel +import app.suhasdissa.vibeyou.backend.viewmodel.PlaylistViewModel import app.suhasdissa.vibeyou.backend.viewmodel.state.AlbumInfoState import app.suhasdissa.vibeyou.ui.components.IllustratedMessageScreen import app.suhasdissa.vibeyou.ui.components.LoadingScreen @@ -51,7 +52,8 @@ import coil.compose.AsyncImage @Composable fun AlbumScreen( state: AlbumInfoState, - playerViewModel: PlayerViewModel = viewModel(factory = PlayerViewModel.Factory) + playerViewModel: PlayerViewModel = viewModel(factory = PlayerViewModel.Factory), + playlistViewModel: PlaylistViewModel = viewModel(factory = PlaylistViewModel.Factory) ) { MiniPlayerScaffold { when (state) { @@ -131,7 +133,10 @@ fun AlbumScreen( FilledTonalButton( modifier = Modifier.fillMaxWidth(), onClick = { - playerViewModel.saveSongs(state.songs) + playlistViewModel.newPlaylistWithSongs( + state.album, + state.songs + ) Toast.makeText( context, context.getString( diff --git a/app/src/main/java/app/suhasdissa/vibeyou/utils/Mappers.kt b/app/src/main/java/app/suhasdissa/vibeyou/utils/Mappers.kt index 52fc90d4..a1e96338 100644 --- a/app/src/main/java/app/suhasdissa/vibeyou/utils/Mappers.kt +++ b/app/src/main/java/app/suhasdissa/vibeyou/utils/Mappers.kt @@ -9,6 +9,7 @@ import androidx.media3.common.MediaMetadata 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.database.entities.PlaylistEntity import app.suhasdissa.vibeyou.backend.database.entities.SongEntity import app.suhasdissa.vibeyou.backend.models.PipedSongResponse import app.suhasdissa.vibeyou.backend.models.playlists.Playlist @@ -61,6 +62,23 @@ val Playlist.asAlbum: Album thumbnailUri = thumbnail.toUri() ) +val Album.asPlaylistEntity: PlaylistEntity + get() = PlaylistEntity( + id = id, + title = title, + type = type, + subTitle = artistsText, + thumbnailUrl = thumbnailUri.toString() + ) +val PlaylistEntity.asAlbum: Album + get() = Album( + id = id, + title = title, + thumbnailUri = thumbnailUrl?.toUri(), + artistsText = subTitle ?: "", + isLocal = true, + type = type + ) val app.suhasdissa.vibeyou.backend.models.artists.Artist.asArtist: Artist get() = Artist( id = artistId, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c46af73d..32edcfb2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,8 +76,9 @@ Added all the songs to the library Add all songs to the library Clear Queue + Playlists Custom instance Api Url Image proxy url Invalid url - \ No newline at end of file +