From 19fa11672da3bfb635bef02645e47f0cfe3143b8 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sun, 25 Sep 2022 15:06:03 +0200 Subject: [PATCH] Add HomeAlbumList --- .../19.json | 608 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 39 +- .../it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt | 7 + .../it/vfsfitvnm/vimusic/models/Album.kt | 3 +- .../{AlbumSongList.kt => AlbumOverview.kt} | 34 +- ...ViewModel.kt => AlbumOverviewViewModel.kt} | 2 +- .../vimusic/ui/screens/home/HomeAlbumList.kt | 187 ++++++ .../ui/screens/home/HomeAlbumListViewModel.kt | 67 ++ .../ui/screens/home/HomePlaylistList.kt | 2 +- .../vimusic/ui/screens/home/HomeScreen.kt | 20 +- .../vimusic/ui/screens/home/HomeSongList.kt | 2 +- .../it/vfsfitvnm/vimusic/utils/Preferences.kt | 3 +- app/src/main/res/drawable/bookmark.xml | 9 + .../main/res/drawable/bookmark_outline.xml | 13 + 14 files changed, 979 insertions(+), 17 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/{AlbumSongList.kt => AlbumOverview.kt} (87%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/{AlbumSongListViewModel.kt => AlbumOverviewViewModel.kt} (97%) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt create mode 100644 app/src/main/res/drawable/bookmark.xml create mode 100644 app/src/main/res/drawable/bookmark_outline.xml diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json new file mode 100644 index 0000000..240b6e0 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json @@ -0,0 +1,608 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "41479c8284963d3533c4baa46d7464a6", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` 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": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronizedLyrics", + "columnName": "synchronizedLyrics", + "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": "SongPlaylistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "playlistId" + ] + }, + "indices": [ + { + "name": "index_SongPlaylistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongPlaylistMap_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shuffleVideoId", + "columnName": "shuffleVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shufflePlaylistId", + "columnName": "shufflePlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioVideoId", + "columnName": "radioVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioPlaylistId", + "columnName": "radioPlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongArtistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_SongArtistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongArtistMap_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorsText", + "columnName": "authorsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrl", + "columnName": "shareUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongAlbumMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "albumId" + ] + }, + "indices": [ + { + "name": "index_SongAlbumMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongAlbumMap_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "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": "QueuedMediaItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaItem", + "columnName": "mediaItem", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [ + { + "viewName": "SortedSongPlaylistMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" + }, + { + "viewName": "SortedSongAlbumMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position" + } + ], + "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, '41479c8284963d3533c4baa46d7464a6')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index e83fbfd..42e2e32 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -28,6 +28,7 @@ import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.Migration import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteDatabase +import it.vfsfitvnm.vimusic.enums.AlbumSortBy import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder @@ -150,6 +151,41 @@ interface Database { @Query("SELECT * FROM Album WHERE id = :id") fun albumWithSongs(id: String): Flow + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC") + fun albumsByTitleAsc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY year ASC") + fun albumsByYearAsc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID ASC") + fun albumsByRowIdAsc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title DESC") + fun albumsByTitleDesc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY year DESC") + fun albumsByYearDesc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID DESC") + fun albumsByRowIdDesc(): Flow> + + fun albums(sortBy: AlbumSortBy, sortOrder: SortOrder): Flow> { + return when (sortBy) { + AlbumSortBy.Title -> when (sortOrder) { + SortOrder.Ascending -> albumsByTitleAsc() + SortOrder.Descending -> albumsByTitleDesc() + } + AlbumSortBy.Year -> when (sortOrder) { + SortOrder.Ascending -> albumsByYearAsc() + SortOrder.Descending -> albumsByYearDesc() + } + AlbumSortBy.DateAdded -> when (sortOrder) { + SortOrder.Ascending -> albumsByRowIdAsc() + SortOrder.Descending -> albumsByRowIdDesc() + } + } + } + @Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id") fun incrementTotalPlayTimeMs(id: String, addition: Long) @@ -362,7 +398,7 @@ interface Database { SortedSongPlaylistMap::class, SortedSongAlbumMap::class ], - version = 18, + version = 19, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -379,6 +415,7 @@ interface Database { AutoMigration(from = 15, to = 16), AutoMigration(from = 16, to = 17), AutoMigration(from = 17, to = 18), + AutoMigration(from = 18, to = 19), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt new file mode 100644 index 0000000..4d99975 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt @@ -0,0 +1,7 @@ +package it.vfsfitvnm.vimusic.enums + +enum class AlbumSortBy { + Title, + Year, + DateAdded +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt index e07b81f..75cc575 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt @@ -13,5 +13,6 @@ data class Album( val year: String? = null, val authorsText: String? = null, val shareUrl: String? = null, - val timestamp: Long? + val timestamp: Long?, + val bookmarkedAt: Long? = null ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt similarity index 87% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index da56efc..e8fd7bd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -39,10 +39,12 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.valentinilk.shimmer.shimmer +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu @@ -68,12 +70,12 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @Composable fun AlbumSongList( browseId: String, - viewModel: AlbumSongListViewModel = viewModel( + viewModel: AlbumOverviewViewModel = viewModel( key = browseId, factory = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return AlbumSongListViewModel(browseId) as T + return AlbumOverviewViewModel(browseId) as T } } ) @@ -121,6 +123,34 @@ fun AlbumSongList( .weight(1f) ) + Image( + painter = painterResource( + if (albumWithSongs.album.bookmarkedAt == null) { + R.drawable.bookmark_outline + } else { + R.drawable.bookmark + } + ), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.accent), + modifier = Modifier + .clickable { + query { + Database.update( + albumWithSongs.album.copy( + bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { + System.currentTimeMillis() + } else { + null + } + ) + ) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + Image( painter = painterResource(R.drawable.share_social), contentDescription = null, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt similarity index 97% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt index ae14d16..7174d59 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -class AlbumSongListViewModel(browseId: String) : ViewModel() { +class AlbumOverviewViewModel(browseId: String) : ViewModel() { var result by mutableStateOf?>(null) private set diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt new file mode 100644 index 0000000..aa77279 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt @@ -0,0 +1,187 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import androidx.annotation.DrawableRes +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.BasicText +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.enums.AlbumSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.styling.Dimensions +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.ui.styling.px +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.thumbnail + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun HomeAlbumList( + onAlbumClick: (Album) -> Unit, + viewModel: HomeAlbumListViewModel = viewModel() +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + + val thumbnailSizeDp = Dimensions.thumbnails.song * 2 + val thumbnailSizePx = thumbnailSizeDp.px + + val sortOrderIconRotation by animateFloatAsState( + targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + animationSpec = tween(durationMillis = 400, easing = LinearEasing) + ) + + val rippleIndication = rememberRipple(bounded = true) + + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header(title = "Albums") { + @Composable + fun Item( + @DrawableRes iconId: Int, + sortBy: AlbumSortBy + ) { + Image( + painter = painterResource(iconId), + contentDescription = null, + colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + modifier = Modifier + .clickable { viewModel.sortBy = sortBy } + .padding(all = 4.dp) + .size(18.dp) + ) + } + + Item( + iconId = R.drawable.calendar, + sortBy = AlbumSortBy.Year + ) + + Item( + iconId = R.drawable.text, + sortBy = AlbumSortBy.Title + ) + + Item( + iconId = R.drawable.time, + sortBy = AlbumSortBy.DateAdded + ) + + Spacer( + modifier = Modifier + .width(2.dp) + ) + + Image( + painter = painterResource(R.drawable.arrow_up), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .padding(all = 4.dp) + .size(18.dp) + .graphicsLayer { rotationZ = sortOrderIconRotation } + ) + } + } + + items( + items = viewModel.items, + key = Album::id + ) { album -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .clickable( + indication = rippleIndication, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onAlbumClick(album) } + ) + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() + .animateItemPlacement() + ) { + AsyncImage( + model = album.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(thumbnailShape) + .size(thumbnailSizeDp) + ) + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + BasicText( + text = album.title ?: "", + style = typography.xs.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + + BasicText( + text = album.authorsText ?: "", + style = typography.xs.semiBold.secondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + + album.year?.let { year -> + BasicText( + text = year, + style = typography.xxs.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(top = 8.dp) + ) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt new file mode 100644 index 0000000..172bf3c --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt @@ -0,0 +1,67 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import android.app.Application +import android.content.SharedPreferences +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.edit +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.enums.AlbumSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.utils.albumSortByKey +import it.vfsfitvnm.vimusic.utils.albumSortOrderKey +import it.vfsfitvnm.vimusic.utils.getEnum +import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf +import it.vfsfitvnm.vimusic.utils.preferences +import it.vfsfitvnm.vimusic.utils.putEnum +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch + +class HomeAlbumListViewModel(application: Application) : AndroidViewModel(application) { + var items by mutableStateOf(emptyList()) + private set + + var sortBy by mutableStatePreferenceOf( + preferences.getEnum( + albumSortByKey, + AlbumSortBy.DateAdded + ) + ) { + preferences.edit { putEnum(albumSortByKey, it) } + collectItems(sortBy = it) + } + + var sortOrder by mutableStatePreferenceOf( + preferences.getEnum( + albumSortOrderKey, + SortOrder.Ascending + ) + ) { + preferences.edit { putEnum(albumSortOrderKey, it) } + collectItems(sortOrder = it) + } + + private var job: Job? = null + + private val preferences: SharedPreferences + get() = getApplication().preferences + + init { + collectItems() + } + + private fun collectItems(sortBy: AlbumSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) { + job?.cancel() + job = viewModelScope.launch { + Database.albums(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { + items = it + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 464b229..3003383 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -145,7 +145,7 @@ fun HomePlaylistList( ) Item( - iconId = R.drawable.calendar, + iconId = R.drawable.time, sortBy = PlaylistSortBy.DateAdded ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 63c7046..f064c1f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -14,7 +14,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.screens.LocalPlaylistScreen -import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResultScreen +import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute @@ -22,6 +22,7 @@ import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen import it.vfsfitvnm.vimusic.ui.screens.searchResultRoute import it.vfsfitvnm.vimusic.ui.screens.searchRoute +import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResultScreen import it.vfsfitvnm.vimusic.ui.screens.settings.SettingsScreen import it.vfsfitvnm.vimusic.ui.screens.settingsRoute import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey @@ -84,7 +85,10 @@ fun HomeScreen() { } host { - val (tabIndex, onTabChanged) = rememberPreference(homeScreenTabIndexKey, defaultValue = 0) + val (tabIndex, onTabChanged) = rememberPreference( + homeScreenTabIndexKey, + defaultValue = 0 + ) Scaffold( topIconButtonId = R.drawable.equalizer, @@ -102,19 +106,17 @@ fun HomeScreen() { ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { when (currentTabIndex) { - 0 -> HomeSongList() 1 -> HomePlaylistList( onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) }, onPlaylistClicked = { localPlaylistRoute(it.id) } ) -// 2 -> ArtistsTab( -// lazyListState = lazyListStates[currentTabIndex], +// 2 -> HomeArtistList( // onArtistClicked = { artistRoute(it.id) } // ) -// 3 -> AlbumsTab( -// lazyListState = lazyListStates[currentTabIndex], -// onAlbumClicked = { albumRoute(it.id) } -// ) + 3 -> HomeAlbumList( + onAlbumClick = { albumRoute(it.id) } + ) + else -> HomeSongList() } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index 2cc16d6..cb64513 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -106,7 +106,7 @@ fun HomeSongList( ) Item( - iconId = R.drawable.calendar, + iconId = R.drawable.time, sortBy = SongSortBy.DateAdded ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt index aafcb71..def091b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt @@ -21,7 +21,8 @@ const val songSortOrderKey = "songSortOrder" const val songSortByKey = "songSortBy" const val playlistSortOrderKey = "playlistSortOrder" const val playlistSortByKey = "playlistSortBy" -const val searchFilterKey = "searchFilter" +const val albumSortOrderKey = "albumSortOrder" +const val albumSortByKey = "albumSortBy" const val repeatModeKey = "repeatMode" const val skipSilenceKey = "skipSilence" const val volumeNormalizationKey = "volumeNormalization" diff --git a/app/src/main/res/drawable/bookmark.xml b/app/src/main/res/drawable/bookmark.xml new file mode 100644 index 0000000..416e06c --- /dev/null +++ b/app/src/main/res/drawable/bookmark.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/bookmark_outline.xml b/app/src/main/res/drawable/bookmark_outline.xml new file mode 100644 index 0000000..1544145 --- /dev/null +++ b/app/src/main/res/drawable/bookmark_outline.xml @@ -0,0 +1,13 @@ + + +