diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json new file mode 100644 index 0000000..ed16244 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json @@ -0,0 +1,614 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "821aa30ff7d14b31e839b2f3b2312f78", + "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, `bookmarkedAt` 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 + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "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, '821aa30ff7d14b31e839b2f3b2312f78')" + ] + } +} \ 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 42e2e32..34b5d65 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -29,6 +29,7 @@ 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.ArtistSortBy import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder @@ -147,6 +148,31 @@ interface Database { @Query("SELECT * FROM Artist WHERE id = :id") fun artist(id: String): Flow + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY name DESC") + fun artistsByNameDesc(): Flow> + + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY name ASC") + fun artistsByNameAsc(): Flow> + + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY ROWID DESC") + fun artistsByRowIdDesc(): Flow> + + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY ROWID ASC") + fun artistsByRowIdAsc(): Flow> + + fun artists(sortBy: ArtistSortBy, sortOrder: SortOrder): Flow> { + return when (sortBy) { + ArtistSortBy.Name -> when (sortOrder) { + SortOrder.Ascending -> artistsByNameAsc() + SortOrder.Descending -> artistsByNameDesc() + } + ArtistSortBy.DateAdded -> when (sortOrder) { + SortOrder.Ascending -> artistsByRowIdAsc() + SortOrder.Descending -> artistsByRowIdDesc() + } + } + } + @Transaction @Query("SELECT * FROM Album WHERE id = :id") fun albumWithSongs(id: String): Flow @@ -398,7 +424,7 @@ interface Database { SortedSongPlaylistMap::class, SortedSongAlbumMap::class ], - version = 19, + version = 20, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -416,6 +442,7 @@ interface Database { AutoMigration(from = 16, to = 17), AutoMigration(from = 17, to = 18), AutoMigration(from = 18, to = 19), + AutoMigration(from = 19, to = 20), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ArtistSortBy.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ArtistSortBy.kt new file mode 100644 index 0000000..2df4053 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ArtistSortBy.kt @@ -0,0 +1,6 @@ +package it.vfsfitvnm.vimusic.enums + +enum class ArtistSortBy { + Name, + DateAdded +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt index bca49ff..9f826f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt @@ -15,5 +15,6 @@ data class Artist( val shufflePlaylistId: String? = null, val radioVideoId: String? = null, val radioPlaylistId: 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/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index e8fd7bd..4ae81d2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -68,7 +68,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalAnimationApi @ExperimentalFoundationApi @Composable -fun AlbumSongList( +fun AlbumOverview( browseId: String, viewModel: AlbumOverviewViewModel = viewModel( key = browseId, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index ac71200..6a77fdc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -29,7 +29,7 @@ fun AlbumScreen(browseId: String) { } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - AlbumSongList(browseId = browseId) + AlbumOverview(browseId = browseId) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt new file mode 100644 index 0000000..e943ccb --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -0,0 +1,308 @@ +package it.vfsfitvnm.vimusic.ui.screens.artist + +import android.content.Intent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +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.height +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.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +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 +import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder +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.ui.styling.shimmer +import it.vfsfitvnm.vimusic.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.color +import it.vfsfitvnm.vimusic.utils.enqueue +import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex +import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.thumbnail + +@ExperimentalAnimationApi +@ExperimentalFoundationApi +@Composable +fun ArtistOverview( + browseId: String, + viewModel: ArtistOverviewViewModel = viewModel( + key = browseId, + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return ArtistOverviewViewModel(browseId) as T + } + } + ) +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + val context = LocalContext.current + + BoxWithConstraints { + val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth + val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px + + viewModel.result?.getOrNull()?.let { albumWithSongs -> + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Column { + Header(title = albumWithSongs.album.title ?: "Unknown") { + if (albumWithSongs.songs.isNotEmpty()) { + BasicText( + text = "Enqueue", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + binder?.player?.enqueue( + albumWithSongs.songs.map(DetailedSong::asMediaItem) + ) + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } + + Spacer( + modifier = Modifier + .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, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + albumWithSongs.album.shareUrl?.let { url -> + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + + context.startActivity( + Intent.createChooser( + sendIntent, + null + ) + ) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + } + + AsyncImage( + model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + ) + } + } + + itemsIndexed( + items = albumWithSongs.songs, + key = { _, song -> song.id } + ) { index, song -> + SongItem( + title = song.title, + authors = song.artistsText ?: albumWithSongs.album.authorsText, + durationText = song.durationText, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + albumWithSongs.songs.map(DetailedSong::asMediaItem), + index + ) + }, + startContent = { + BasicText( + text = "${index + 1}", + style = typography.s.semiBold.center.color(colorPalette.textDisabled), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .width(Dimensions.thumbnails.song) + ) + }, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + ) + } + } + + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + albumWithSongs.songs + .shuffled() + .map(DetailedSong::asMediaItem) + ) + } + .background(colorPalette.background2) + .size(62.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp) + ) + } + } ?: viewModel.result?.exceptionOrNull()?.let { + Box( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + viewModel.fetch(browseId) + } + } + .align(Alignment.Center) + .fillMaxSize() + ) { + BasicText( + text = "An error has occurred.\nTap to retry", + style = typography.s.medium.secondary.center, + modifier = Modifier + .align(Alignment.Center) + ) + } + } ?: Column( + modifier = Modifier + .padding(LocalPlayerAwarePaddingValues.current) + .shimmer() + ) { + HeaderPlaceholder() + + Spacer( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + .background(colorPalette.shimmer) + ) + + repeat(3) { index -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .alpha(1f - index * 0.25f) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) + .height(Dimensions.thumbnails.song) + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(Dimensions.thumbnails.song) + ) + + Column { + TextPlaceholder() + TextPlaceholder() + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt new file mode 100644 index 0000000..ccf0efb --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt @@ -0,0 +1,66 @@ +package it.vfsfitvnm.vimusic.ui.screens.artist + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.models.AlbumWithSongs +import it.vfsfitvnm.vimusic.models.SongAlbumMap +import it.vfsfitvnm.vimusic.utils.toMediaItem +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class ArtistOverviewViewModel(browseId: String) : ViewModel() { + var result by mutableStateOf?>(null) + private set + + private var job: Job? = null + + init { + fetch(browseId) + } + + fun fetch(browseId: String) { + job?.cancel() + result = null + + job = viewModelScope.launch(Dispatchers.IO) { + Database.albumWithSongs(browseId).collect { albumWithSongs -> + result = if (albumWithSongs?.album?.timestamp == null) { + YouTube.album(browseId)?.map { youtubeAlbum -> + Database.upsert( + Album( + id = browseId, + title = youtubeAlbum.title, + thumbnailUrl = youtubeAlbum.thumbnail?.url, + year = youtubeAlbum.year, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + shareUrl = youtubeAlbum.url, + timestamp = System.currentTimeMillis() + ), + youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> + albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> + Database.insert(mediaItem) + SongAlbumMap( + songId = mediaItem.mediaId, + albumId = browseId, + position = position + ) + } + } ?: emptyList() + ) + + null + } + } else { + Result.success(albumWithSongs) + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt similarity index 92% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index 26638b7..b47a102 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -1,7 +1,8 @@ -package it.vfsfitvnm.vimusic.ui.screens +package it.vfsfitvnm.vimusic.ui.screens.artist import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -25,7 +26,10 @@ import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -49,7 +53,10 @@ import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder +import it.vfsfitvnm.vimusic.ui.screens.album.AlbumOverview +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -70,9 +77,39 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +@OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @Composable -fun ArtistScreen(browseId: String) { +fun AlbumScreen(browseId: String) { + val saveableStateHolder = rememberSaveableStateHolder() + val (tabIndex, onTabIndexChanged) = rememberSaveable { + mutableStateOf(0) + } + + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + host { + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = tabIndex, + onTabChanged = onTabIndexChanged, + tabColumnContent = { Item -> + Item(0, "Overview", R.drawable.sparkles) + } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { + ArtistOverview(browseId = browseId) + } + } + } + } +} + +@ExperimentalAnimationApi +@Composable +fun ArtistScreen2(browseId: String) { val lazyListState = rememberLazyListState() RouteHandler(listenToGlobalEmitter = true) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt new file mode 100644 index 0000000..110231b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt @@ -0,0 +1,171 @@ +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +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.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.ArtistSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Artist +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.center +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.thumbnail + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun HomeArtistList( + onArtistClick: (Artist) -> Unit, + viewModel: HomeArtistListViewModel = viewModel() +) { + val (colorPalette, typography) = 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) + + LazyVerticalGrid( + columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2), + contentPadding = LocalPlayerAwarePaddingValues.current, + verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2), + horizontalArrangement = Arrangement.spacedBy( + space = Dimensions.itemsVerticalPadding * 2, + alignment = Alignment.CenterHorizontally + ), + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0, + span = { GridItemSpan(maxLineSpan) } + ) { + Header(title = "Artists") { + @Composable + fun Item( + @DrawableRes iconId: Int, + sortBy: ArtistSortBy + ) { + 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.text, + sortBy = ArtistSortBy.Name + ) + + Item( + iconId = R.drawable.time, + sortBy = ArtistSortBy.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 = Artist::id + ) { artist -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .requiredWidth(thumbnailSizeDp) + .animateItemPlacement() + ) { + AsyncImage( + model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable( + indication = rippleIndication, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onArtistClick(artist) } + ) + .background(colorPalette.background1) + .align(Alignment.CenterHorizontally) + .requiredSize(thumbnailSizeDp), + ) + + BasicText( + text = artist.name, + style = typography.xxs.semiBold.center, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt new file mode 100644 index 0000000..e733957 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.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.ArtistSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Artist +import it.vfsfitvnm.vimusic.utils.artistSortByKey +import it.vfsfitvnm.vimusic.utils.artistSortOrderKey +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 HomeArtistListViewModel(application: Application) : AndroidViewModel(application) { + var items by mutableStateOf(emptyList()) + private set + + var sortBy by mutableStatePreferenceOf( + preferences.getEnum( + artistSortByKey, + ArtistSortBy.DateAdded + ) + ) { + preferences.edit { putEnum(artistSortByKey, it) } + collectItems(sortBy = it) + } + + var sortOrder by mutableStatePreferenceOf( + preferences.getEnum( + artistSortOrderKey, + SortOrder.Ascending + ) + ) { + preferences.edit { putEnum(artistSortOrderKey, it) } + collectItems(sortOrder = it) + } + + private var job: Job? = null + + private val preferences: SharedPreferences + get() = getApplication().preferences + + init { + collectItems() + } + + private fun collectItems(sortBy: ArtistSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) { + job?.cancel() + job = viewModelScope.launch { + Database.artists(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { + items = it + } + } + } +} 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 f064c1f..1b7192a 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 @@ -15,6 +15,7 @@ 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.albumRoute +import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute @@ -110,12 +111,15 @@ fun HomeScreen() { onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) }, onPlaylistClicked = { localPlaylistRoute(it.id) } ) -// 2 -> HomeArtistList( -// onArtistClicked = { artistRoute(it.id) } -// ) + + 2 -> HomeArtistList( + onArtistClick = { artistRoute(it.id) } + ) + 3 -> HomeAlbumList( onAlbumClick = { albumRoute(it.id) } ) + else -> HomeSongList() } } 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 def091b..2cd3eb2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt @@ -23,6 +23,8 @@ const val playlistSortOrderKey = "playlistSortOrder" const val playlistSortByKey = "playlistSortBy" const val albumSortOrderKey = "albumSortOrder" const val albumSortByKey = "albumSortBy" +const val artistSortOrderKey = "artistSortOrder" +const val artistSortByKey = "artistSortBy" const val repeatModeKey = "repeatMode" const val skipSilenceKey = "skipSilence" const val volumeNormalizationKey = "volumeNormalization"