diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/3.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/3.json new file mode 100644 index 0000000..6ef56c2 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/3.json @@ -0,0 +1,342 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "f2169b1328eebb0c7f353018e2ae4bd3", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `albumInfoId` INTEGER, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` 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": "albumInfoId", + "columnName": "albumInfoId", + "affinity": "INTEGER", + "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": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongInPlaylist", + "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_SongInPlaylist_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongInPlaylist_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongInPlaylist_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongInPlaylist_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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `browseId` TEXT, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongWithAuthors", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `authorInfoId` INTEGER NOT NULL, PRIMARY KEY(`songId`, `authorInfoId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`authorInfoId`) REFERENCES `Info`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "authorInfoId", + "columnName": "authorInfoId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "authorInfoId" + ] + }, + "indices": [ + { + "name": "index_SongWithAuthors_authorInfoId", + "unique": false, + "columnNames": [ + "authorInfoId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongWithAuthors_authorInfoId` ON `${TABLE_NAME}` (`authorInfoId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Info", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "authorInfoId" + ], + "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": [] + } + ], + "views": [ + { + "viewName": "SortedSongInPlaylist", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongInPlaylist 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, 'f2169b1328eebb0c7f353018e2ae4bd3')" + ] + } +} \ 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 97d6ca2..9e0c445 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -7,7 +7,6 @@ import androidx.media3.common.MediaItem import androidx.room.* import it.vfsfitvnm.vimusic.models.* import kotlinx.coroutines.flow.Flow -import java.io.ByteArrayOutputStream @Dao @@ -125,15 +124,22 @@ interface Database { @androidx.room.Database( entities = [ - Song::class, SongInPlaylist::class, Playlist::class, Info::class, SongWithAuthors::class, SearchQuery::class, QueuedMediaItem::class + Song::class, + SongInPlaylist::class, + Playlist::class, + Info::class, + SongWithAuthors::class, + SearchQuery::class, + QueuedMediaItem::class ], views = [ SortedSongInPlaylist::class ], - version = 2, + version = 3, exportSchema = true, autoMigrations = [ - AutoMigration(from = 1, to = 2) + AutoMigration(from = 1, to = 2), + AutoMigration(from = 2, to = 3), ] ) @TypeConverters(Converters::class) @@ -162,7 +168,7 @@ object Converters { return value?.let { byteArray -> val parcel = Parcel.obtain() parcel.unmarshall(byteArray, 0, byteArray.size) - parcel.setDataPosition(0); + parcel.setDataPosition(0) val pb = parcel.readBundle(MediaItem::class.java.classLoader) parcel.recycle() diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt index c6a9ea6..c9f5f8f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Song.kt @@ -10,8 +10,9 @@ data class Song( val albumInfoId: Long?, val durationText: String, val thumbnailUrl: String?, + val lyrics: String? = null, val likedAt: Long? = null, - val totalPlayTimeMs: Long = 0 + val totalPlayTimeMs: Long = 0, ) { val formattedTotalPlayTime: String get() { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt index 0d6d32b..2f9377a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt @@ -26,7 +26,9 @@ import it.vfsfitvnm.route.Route import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.empty import it.vfsfitvnm.route.rememberRoute +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheetState import it.vfsfitvnm.vimusic.ui.components.Message @@ -35,10 +37,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.screens.rememberLyricsRoute import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography -import it.vfsfitvnm.vimusic.utils.LocalYoutubePlayer -import it.vfsfitvnm.vimusic.utils.center -import it.vfsfitvnm.vimusic.utils.color -import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.youtubemusic.Outcome import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.isEvaluable @@ -51,6 +50,7 @@ import kotlinx.coroutines.withContext @Composable fun PlayerBottomSheet( layoutState: BottomSheetState, + song: Song?, onGlobalRouteEmitted: () -> Unit, modifier: Modifier = Modifier, ) { @@ -69,10 +69,6 @@ fun PlayerBottomSheet( mutableStateOf>(Outcome.Initial) } - var lyricsOutcome by remember(player.mediaItem!!.mediaId) { - mutableStateOf>(Outcome.Initial) - } - BottomSheet( state = layoutState, peekHeight = 128.dp, @@ -91,7 +87,10 @@ fun PlayerBottomSheet( Spacer( modifier = Modifier .align(Alignment.CenterHorizontally) - .background(color = colorPalette.textDisabled, shape = RoundedCornerShape(16.dp)) + .background( + color = colorPalette.textDisabled, + shape = RoundedCornerShape(16.dp) + ) .width(36.dp) .height(4.dp) .padding(top = 8.dp) @@ -175,13 +174,18 @@ fun PlayerBottomSheet( .background(colorPalette.elevatedBackground) .fillMaxSize() ) { + var lyricsOutcome by remember(song) { + mutableStateOf(song?.lyrics?.let { Outcome.Success(it) } ?: Outcome.Initial) + } + lyricsRoute { OutcomeItem( outcome = lyricsOutcome, onInitialize = { - lyricsOutcome = Outcome.Loading - + println("onInitialize!!") coroutineScope.launch(Dispatchers.Main) { + lyricsOutcome = Outcome.Loading + if (nextOutcome.isEvaluable) { nextOutcome = Outcome.Loading nextOutcome = withContext(Dispatchers.IO) { @@ -195,6 +199,13 @@ fun PlayerBottomSheet( lyricsOutcome = nextOutcome.flatMap { it.lyrics?.text().toNotNull() + }.map { + it ?: "" + }.map { + withContext(Dispatchers.IO) { + Database.update((song ?: Database.insert(player.mediaItem!!)).copy(lyrics = it)) + } + it } } }, @@ -205,7 +216,14 @@ fun PlayerBottomSheet( ) } ) { lyrics -> - if (lyrics != null) { + if (lyrics.isEmpty()) { + Message( + text = "Lyrics not available", + icon = R.drawable.text, + modifier = Modifier + .padding(top = 64.dp) + ) + } else { BasicText( text = lyrics, style = typography.xs.center, @@ -217,13 +235,6 @@ fun PlayerBottomSheet( .padding(vertical = 16.dp) .padding(horizontal = 48.dp) ) - } else { - Message( - text = "Lyrics not available", - icon = R.drawable.text, - modifier = Modifier - .padding(top = 64.dp) - ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt index 54656ef..aebc378 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt @@ -68,11 +68,6 @@ fun PlayerView( size to density.run { size.minus(64.dp).roundToPx() } } - val song by remember(player.mediaItem?.mediaId) { - player.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged() ?: flowOf(null) - }.collectAsState(initial = null, context = Dispatchers.IO) - - BottomSheet( state = layoutState, modifier = modifier, @@ -169,6 +164,10 @@ fun PlayerView( } } ) { + val song by remember(player.mediaItem?.mediaId) { + player.mediaItem?.mediaId?.let(Database::songFlow)?.distinctUntilChanged() ?: flowOf(null) + }.collectAsState(initial = null, context = Dispatchers.IO) + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -440,6 +439,7 @@ fun PlayerView( PlayerBottomSheet( layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound - 128.dp), onGlobalRouteEmitted = layoutState.collapse, + song = song, modifier = Modifier .padding(bottom = 128.dp) .align(Alignment.BottomCenter)