diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 165754e..5687011 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -84,7 +84,6 @@ dependencies { implementation(libs.compose.ripple) implementation(libs.compose.shimmer) implementation(libs.compose.coil) - implementation(libs.compose.viewmodel) implementation(libs.palette) diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json index 00613b9..e0c912d 100644 --- a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 18, - "identityHash": "dec162db7ec49f4324481d54c49a793d", + "identityHash": "c8f776e899b181081f0230bffec99ac5", "entities": [ { "tableName": "Song", @@ -181,7 +181,7 @@ }, { "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`))", + "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", @@ -236,6 +236,12 @@ "columnName": "timestamp", "affinity": "INTEGER", "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -318,7 +324,7 @@ }, { "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, PRIMARY KEY(`id`))", + "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", @@ -361,6 +367,12 @@ "columnName": "timestamp", "affinity": "INTEGER", "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -588,15 +600,11 @@ { "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, 'dec162db7ec49f4324481d54c49a793d')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c8f776e899b181081f0230bffec99ac5')" ] } } \ No newline at end of file diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json deleted file mode 100644 index 240b6e0..0000000 --- a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json +++ /dev/null @@ -1,608 +0,0 @@ -{ - "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/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json deleted file mode 100644 index ed16244..0000000 --- a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json +++ /dev/null @@ -1,614 +0,0 @@ -{ - "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 34b5d65..ea007bb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -34,7 +34,6 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.Album -import it.vfsfitvnm.vimusic.models.AlbumWithSongs import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength @@ -48,7 +47,6 @@ import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.models.SongAlbumMap import it.vfsfitvnm.vimusic.models.SongArtistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.models.SortedSongAlbumMap import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -173,9 +171,13 @@ interface Database { } } - @Transaction @Query("SELECT * FROM Album WHERE id = :id") - fun albumWithSongs(id: String): Flow + fun album(id: String): Flow + + @Transaction + @Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position") + @RewriteQueriesToDropUnusedColumns + fun albumSongs(albumId: String): Flow> @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC") fun albumsByTitleAsc(): Flow> @@ -421,10 +423,9 @@ interface Database { Format::class, ], views = [ - SortedSongPlaylistMap::class, - SortedSongAlbumMap::class + SortedSongPlaylistMap::class ], - version = 20, + version = 18, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -441,8 +442,6 @@ interface Database { AutoMigration(from = 15, to = 16), 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/models/AlbumWithSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt deleted file mode 100644 index 0ca36e5..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt +++ /dev/null @@ -1,22 +0,0 @@ -package it.vfsfitvnm.vimusic.models - -import androidx.compose.runtime.Immutable -import androidx.room.Embedded -import androidx.room.Junction -import androidx.room.Relation - -@Immutable -data class AlbumWithSongs( - @Embedded val album: Album, - @Relation( - entity = Song::class, - parentColumn = "id", - entityColumn = "id", - associateBy = Junction( - value = SortedSongAlbumMap::class, - parentColumn = "albumId", - entityColumn = "songId" - ) - ) - val songs: List -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt deleted file mode 100644 index b41b451..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt +++ /dev/null @@ -1,13 +0,0 @@ -package it.vfsfitvnm.vimusic.models - -import androidx.compose.runtime.Immutable -import androidx.room.ColumnInfo -import androidx.room.DatabaseView - -@Immutable -@DatabaseView("SELECT * FROM SongAlbumMap ORDER BY position") -data class SortedSongAlbumMap( - @ColumnInfo(index = true) val songId: String, - @ColumnInfo(index = true) val albumId: String, - val position: Int -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt new file mode 100644 index 0000000..b0c86d0 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val AlbumListSaver = ListSaver.of(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt new file mode 100644 index 0000000..4b9eea3 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val AlbumResultSaver = ResultSaver.of(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt new file mode 100644 index 0000000..2f88b36 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt @@ -0,0 +1,29 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Album + +object AlbumSaver : Saver> { + override fun SaverScope.save(value: Album): List = listOf( + value.id, + value.title, + value.thumbnailUrl, + value.year, + value.authorsText, + value.shareUrl, + value.timestamp, + value.bookmarkedAt, + ) + + override fun restore(value: List): Album = Album( + id = value[0] as String, + title = value[1] as String, + thumbnailUrl = value[2] as String?, + year = value[3] as String?, + authorsText = value[4] as String?, + shareUrl = value[5] as String?, + timestamp = value[6] as Long?, + bookmarkedAt = value[7] as Long?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt new file mode 100644 index 0000000..125d725 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val ArtistListSaver = ListSaver.of(ArtistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt new file mode 100644 index 0000000..a609450 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt @@ -0,0 +1,34 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Artist +import it.vfsfitvnm.vimusic.models.Playlist + +object ArtistSaver : Saver> { + override fun SaverScope.save(value: Artist): List = listOf( + value.id, + value.name, + value.thumbnailUrl, + value.info, + value.shuffleVideoId, + value.shufflePlaylistId, + value.radioVideoId, + value.radioPlaylistId, + value.timestamp, + value.bookmarkedAt, + ) + + override fun restore(value: List): Artist = Artist( + id = value[0] as String, + name = value[1] as String, + thumbnailUrl = value[2] as String?, + info = value[3] as String?, + shuffleVideoId = value[4] as String?, + shufflePlaylistId = value[5] as String?, + radioVideoId = value[6] as String?, + radioPlaylistId = value[7] as String?, + timestamp = value[8] as Long?, + bookmarkedAt = value[9] as Long?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt new file mode 100644 index 0000000..cdca8e8 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val DetailedSongListSaver = ListSaver.of(DetailedSongSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt new file mode 100644 index 0000000..cfede7b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt @@ -0,0 +1,33 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.DetailedSong + +object DetailedSongSaver : Saver> { + override fun SaverScope.save(value: DetailedSong): List = + listOf( + value.id, + value.title, + value.artistsText, + value.durationText, + value.thumbnailUrl, + value.totalPlayTimeMs, + value.albumId, + value.artists?.let { with(InfoListSaver) { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List): DetailedSong? { + return if (value.size == 8) DetailedSong( + id = value[0] as String, + title = value[1] as String, + artistsText = value[2] as String?, + durationText = value[3] as String, + thumbnailUrl = value[4] as String?, + totalPlayTimeMs = value[5] as Long, + albumId = value[6] as String?, + artists = InfoListSaver.restore(value[7] as List>) + ) else null + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt new file mode 100644 index 0000000..8c3347f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val InfoListSaver = ListSaver.of(InfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt new file mode 100644 index 0000000..3f59e7b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt @@ -0,0 +1,16 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Info + +object InfoSaver : Saver> { + override fun SaverScope.save(value: Info): List = listOf(value.id, value.name) + + override fun restore(value: List): Info? { + return if (value.size == 2) Info( + id = value[0], + name = value[1], + ) else null + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt new file mode 100644 index 0000000..3b7f617 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt @@ -0,0 +1,20 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope + +interface ListSaver : Saver, List> { + companion object { + fun of(saver: Saver): ListSaver { + return object : ListSaver { + override fun restore(value: List): List { + return value.mapNotNull(saver::restore) + } + + override fun SaverScope.save(value: List): List { + return with(saver) { value.mapNotNull { save(it) } } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt new file mode 100644 index 0000000..6d726e3 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val PlaylistPreviewListSaver = ListSaver.of(PlaylistPreviewSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt new file mode 100644 index 0000000..5641d4f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt @@ -0,0 +1,21 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.PlaylistPreview + +object PlaylistPreviewSaver : Saver> { + override fun SaverScope.save(value: PlaylistPreview): List { + return listOf( + with(PlaylistSaver) { save(value.playlist) }, + value.songCount, + ) + } + + override fun restore(value: List): PlaylistPreview? { + return if (value.size == 2) PlaylistPreview( + playlist = PlaylistSaver.restore(value[0] as List), + songCount = value[1] as Int, + ) else null + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistSaver.kt new file mode 100644 index 0000000..c660f57 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistSaver.kt @@ -0,0 +1,19 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Playlist + +object PlaylistSaver : Saver> { + override fun SaverScope.save(value: Playlist): List = listOf( + value.id, + value.name, + value.browseId, + ) + + override fun restore(value: List): Playlist = Playlist( + id = value[0] as Long, + name = value[1] as String, + browseId = value[2] as String?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt new file mode 100644 index 0000000..e750098 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope + +interface ResultSaver : Saver?, Pair> { + companion object { + fun of(saver: Saver) = + object : Saver?, Pair> { + override fun restore(value: Pair) = + value.first?.let(saver::restore)?.let(Result.Companion::success) + ?: value.second?.let(Result.Companion::failure) + + override fun SaverScope.save(value: Result?) = + with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull() + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt new file mode 100644 index 0000000..9e580f5 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val SearchQueryListSaver = ListSaver.of(SearchQuerySaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt new file mode 100644 index 0000000..57df500 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt @@ -0,0 +1,17 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.SearchQuery + +object SearchQuerySaver : Saver> { + override fun SaverScope.save(value: SearchQuery): List = listOf( + value.id, + value.query, + ) + + override fun restore(value: List) = SearchQuery( + id = value[0] as Long, + query = value[1] as String + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt new file mode 100644 index 0000000..f7da5d5 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt @@ -0,0 +1,5 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.autoSaver + +val StringListResultSaver = ResultSaver.of(autoSaver?>()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt new file mode 100644 index 0000000..a5b35aa --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt @@ -0,0 +1,5 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.autoSaver + +val StringResultSaver = ResultSaver.of(autoSaver()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt new file mode 100644 index 0000000..b7c6f0a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeAlbumListSaver = ListSaver.of(YouTubeAlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt new file mode 100644 index 0000000..c4a5f8d --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt @@ -0,0 +1,22 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeAlbumSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Album): List = listOf( + with(YouTubeBrowseInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.year, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Item.Album( + info = YouTubeBrowseInfoSaver.restore(value[0] as List), + authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), + year = value[2] as String?, + thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt new file mode 100644 index 0000000..07d70fc --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeArtistListSaver = ListSaver.of(YouTubeArtistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt new file mode 100644 index 0000000..98a1965 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt @@ -0,0 +1,19 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeArtistSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Artist): List = listOf( + with(YouTubeBrowseInfoSaver) { save(value.info) }, + value.subscribersCountText, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Item.Artist( + info = YouTubeBrowseInfoSaver.restore(value[0] as List), + subscribersCountText = value[1] as String?, + thumbnail = (value[2] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt new file mode 100644 index 0000000..30aa186 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeBrowseEndpointSaver : Saver> { + override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf( + value.browseId, + value.params + ) + + override fun restore(value: List) = NavigationEndpoint.Endpoint.Browse( + browseId = value[0] as String, + params = value[1] as String?, + browseEndpointContextSupportedConfigs = null + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt new file mode 100644 index 0000000..6d700e9 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeBrowseInfoListSaver = ListSaver.of(YouTubeBrowseInfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt new file mode 100644 index 0000000..a421e53 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeBrowseInfoSaver : Saver, List> { + override fun SaverScope.save(value: YouTube.Info) = listOf( + value.name, + with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Info( + name = value[0] as String, + endpoint = (value[1] as List?)?.let(YouTubeBrowseEndpointSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt new file mode 100644 index 0000000..7fe7d81 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubePlaylistListSaver = ListSaver.of(YouTubePlaylistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt new file mode 100644 index 0000000..9767efe --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt @@ -0,0 +1,21 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubePlaylistSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Playlist): List = listOf( + with(YouTubeBrowseInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoSaver) { value.channel?.let { save(it) } }, + value.songCount, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Item.Playlist( + info = YouTubeBrowseInfoSaver.restore(value[0] as List), + channel = (value[1] as List?)?.let(YouTubeBrowseInfoSaver::restore), + songCount = value[2] as Int?, + thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt new file mode 100644 index 0000000..8dade3e --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeSongListSaver = ListSaver.of(YouTubeSongSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt new file mode 100644 index 0000000..4b8cdf8 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt @@ -0,0 +1,24 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeSongSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Song): List = listOf( + with(YouTubeWatchInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + with(YouTubeBrowseInfoSaver) { value.album?.let { save(it) } }, + value.durationText, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Item.Song( + info = YouTubeWatchInfoSaver.restore(value[0] as List), + authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + album = (value[2] as List?)?.let(YouTubeBrowseInfoSaver::restore), + durationText = value[3] as String?, + thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt new file mode 100644 index 0000000..d5664a4 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt @@ -0,0 +1,19 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer + +object YouTubeThumbnailSaver : Saver> { + override fun SaverScope.save(value: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail) = listOf( + value.url, + value.width, + value.height + ) + + override fun restore(value: List) = ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail( + url = value[0] as String, + width = value[1] as Int, + height = value[2] as Int?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt new file mode 100644 index 0000000..2e05f70 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeVideoListSaver = ListSaver.of(YouTubeVideoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt new file mode 100644 index 0000000..b745939 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt @@ -0,0 +1,24 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeVideoSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Video): List = listOf( + with(YouTubeWatchInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.viewsText, + value.durationText, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Item.Video( + info = YouTubeWatchInfoSaver.restore(value[0] as List), + authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + viewsText = value[2] as String?, + durationText = value[3] as String?, + thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt new file mode 100644 index 0000000..5548bcf --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt @@ -0,0 +1,24 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeWatchEndpointSaver : Saver> { + override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf( + value.params, + value.playlistId, + value.videoId, + value.index, + value.playlistSetVideoId, + ) + + override fun restore(value: List) = NavigationEndpoint.Endpoint.Watch( + params = value[0] as String?, + playlistId = value[1] as String?, + videoId = value[2] as String?, + index = value[3] as Int?, + playlistSetVideoId = value[4] as String?, + watchEndpointMusicSupportedConfigs = null + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt new file mode 100644 index 0000000..a563724 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeWatchInfoSaver : Saver, List> { + override fun SaverScope.save(value: YouTube.Info) = listOf( + value.name, + with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Info( + name = value[0] as String, + endpoint = (value[1] as List?)?.let(YouTubeWatchEndpointSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt index d41a827..8f8e16f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt @@ -9,6 +9,7 @@ import it.vfsfitvnm.route.Route1 import it.vfsfitvnm.route.RouteHandlerScope import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen +import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen val albumRoute = Route1("albumRoute") val artistRoute = Route1("artistRoute") 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 4ae81d2..fdffff9 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 @@ -24,6 +24,7 @@ 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.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -34,17 +35,16 @@ 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.Album import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu @@ -61,6 +61,7 @@ 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.produceSaveableListState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail @@ -69,26 +70,26 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalFoundationApi @Composable fun AlbumOverview( + albumResult: Result?, browseId: String, - viewModel: AlbumOverviewViewModel = viewModel( - key = browseId, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return AlbumOverviewViewModel(browseId) as T - } - } - ) ) { val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val context = LocalContext.current + + val songs by produceSaveableListState( + flowProvider = { + Database.albumSongs(browseId) + }, + stateSaver = DetailedSongListSaver + + ) BoxWithConstraints { val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px - viewModel.result?.getOrNull()?.let { albumWithSongs -> + albumResult?.getOrNull()?.let { album -> LazyColumn( contentPadding = LocalPlayerAwarePaddingValues.current, modifier = Modifier @@ -100,8 +101,8 @@ fun AlbumOverview( contentType = 0 ) { Column { - Header(title = albumWithSongs.album.title ?: "Unknown") { - if (albumWithSongs.songs.isNotEmpty()) { + Header(title = album.title ?: "Unknown") { + if (songs.isNotEmpty()) { BasicText( text = "Enqueue", style = typography.xxs.medium, @@ -109,7 +110,7 @@ fun AlbumOverview( .clip(RoundedCornerShape(16.dp)) .clickable { binder?.player?.enqueue( - albumWithSongs.songs.map(DetailedSong::asMediaItem) + songs.map(DetailedSong::asMediaItem) ) } .background(colorPalette.background2) @@ -125,7 +126,7 @@ fun AlbumOverview( Image( painter = painterResource( - if (albumWithSongs.album.bookmarkedAt == null) { + if (album.bookmarkedAt == null) { R.drawable.bookmark_outline } else { R.drawable.bookmark @@ -137,8 +138,8 @@ fun AlbumOverview( .clickable { query { Database.update( - albumWithSongs.album.copy( - bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { + album.copy( + bookmarkedAt = if (album.bookmarkedAt == null) { System.currentTimeMillis() } else { null @@ -157,7 +158,7 @@ fun AlbumOverview( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - albumWithSongs.album.shareUrl?.let { url -> + album.shareUrl?.let { url -> val sendIntent = Intent().apply { action = Intent.ACTION_SEND type = "text/plain" @@ -178,7 +179,7 @@ fun AlbumOverview( } AsyncImage( - model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), + model = album.thumbnailUrl?.thumbnail(thumbnailSizePx), contentDescription = null, modifier = Modifier .align(Alignment.CenterHorizontally) @@ -190,17 +191,17 @@ fun AlbumOverview( } itemsIndexed( - items = albumWithSongs.songs, + items = songs, key = { _, song -> song.id } ) { index, song -> SongItem( title = song.title, - authors = song.artistsText ?: albumWithSongs.album.authorsText, + authors = song.artistsText ?: album.authorsText, durationText = song.durationText, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex( - albumWithSongs.songs.map(DetailedSong::asMediaItem), + songs.map(DetailedSong::asMediaItem), index ) }, @@ -227,10 +228,10 @@ fun AlbumOverview( .padding(all = 16.dp) .padding(LocalPlayerAwarePaddingValues.current) .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { + .clickable(enabled = songs.isNotEmpty()) { binder?.stopRadio() binder?.player?.forcePlayFromBeginning( - albumWithSongs.songs + songs .shuffled() .map(DetailedSong::asMediaItem) ) @@ -247,12 +248,12 @@ fun AlbumOverview( .size(20.dp) ) } - } ?: viewModel.result?.exceptionOrNull()?.let { + } ?: albumResult?.exceptionOrNull()?.let { Box( modifier = Modifier .pointerInput(Unit) { detectTapGestures { - viewModel.fetch(browseId) +// viewModel.fetch(browseId) } } .align(Alignment.Center) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt deleted file mode 100644 index 7174d59..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt +++ /dev/null @@ -1,66 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.album - -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 AlbumOverviewViewModel(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/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index 6a77fdc..530a211 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 @@ -3,11 +3,21 @@ package it.vfsfitvnm.vimusic.ui.screens.album import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.saveable.rememberSaveableStateHolder import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.models.SongAlbumMap +import it.vfsfitvnm.vimusic.savers.AlbumResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.globalRoutes +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import it.vfsfitvnm.vimusic.utils.toMediaItem +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @@ -19,6 +29,45 @@ fun AlbumScreen(browseId: String) { globalRoutes() host { + val albumResult by produceSaveableState( + initialValue = null, + stateSaver = AlbumResultSaver, + ) { + withContext(Dispatchers.IO) { + Database.album(browseId).collect { album -> + if (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 { + value = Result.success(album) + } + } + } + } + Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, @@ -29,7 +78,10 @@ fun AlbumScreen(browseId: String) { } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - AlbumOverview(browseId = browseId) + AlbumOverview( + albumResult = albumResult, + 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 index e943ccb..ba04073 100644 --- 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 @@ -36,7 +36,6 @@ 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 @@ -70,239 +69,230 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @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() - } - } - } - } - } +// 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 deleted file mode 100644 index ccf0efb..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt +++ /dev/null @@ -1,66 +0,0 @@ -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/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index b47a102..e281687 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -80,7 +80,7 @@ import kotlinx.coroutines.runBlocking @OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @Composable -fun AlbumScreen(browseId: String) { +fun ArtistScreen(browseId: String) { val saveableStateHolder = rememberSaveableStateHolder() val (tabIndex, onTabIndexChanged) = rememberSaveable { mutableStateOf(0) 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 index aa77279..b025763 100644 --- 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 @@ -26,6 +26,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -35,17 +36,22 @@ 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.Database 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.savers.AlbumListSaver 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.albumSortByKey +import it.vfsfitvnm.vimusic.utils.albumSortOrderKey +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail @@ -54,16 +60,25 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalAnimationApi @Composable fun HomeAlbumList( - onAlbumClick: (Album) -> Unit, - viewModel: HomeAlbumListViewModel = viewModel() + onAlbumClick: (Album) -> Unit ) { val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded) + var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.albums(sortBy, sortOrder) }, + stateSaver = AlbumListSaver, + key1 = sortBy, + key2 = sortOrder + ) + val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizePx = thumbnailSizeDp.px val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -83,14 +98,14 @@ fun HomeAlbumList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: AlbumSortBy + targetSortBy: AlbumSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -98,17 +113,17 @@ fun HomeAlbumList( Item( iconId = R.drawable.calendar, - sortBy = AlbumSortBy.Year + targetSortBy = AlbumSortBy.Year ) Item( iconId = R.drawable.text, - sortBy = AlbumSortBy.Title + targetSortBy = AlbumSortBy.Title ) Item( iconId = R.drawable.time, - sortBy = AlbumSortBy.DateAdded + targetSortBy = AlbumSortBy.DateAdded ) Spacer( @@ -121,7 +136,7 @@ fun HomeAlbumList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -130,7 +145,7 @@ fun HomeAlbumList( } items( - items = viewModel.items, + items = items, key = Album::id ) { album -> Row( 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 deleted file mode 100644 index 172bf3c..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt +++ /dev/null @@ -1,67 +0,0 @@ -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/HomeArtistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt index 110231b..9730504 100644 --- 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 @@ -29,6 +29,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -37,18 +38,23 @@ 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.Database 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.savers.ArtistListSaver 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.artistSortByKey +import it.vfsfitvnm.vimusic.utils.artistSortOrderKey import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail @@ -56,16 +62,25 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalAnimationApi @Composable fun HomeArtistList( - onArtistClick: (Artist) -> Unit, - viewModel: HomeArtistListViewModel = viewModel() + onArtistClick: (Artist) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current + var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded) + var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.artists(sortBy, sortOrder) }, + stateSaver = ArtistListSaver, + key1 = sortBy, + key2 = sortOrder + ) + val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizePx = thumbnailSizeDp.px val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -92,14 +107,14 @@ fun HomeArtistList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: ArtistSortBy + targetSortBy: ArtistSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -107,12 +122,12 @@ fun HomeArtistList( Item( iconId = R.drawable.text, - sortBy = ArtistSortBy.Name + targetSortBy = ArtistSortBy.Name ) Item( iconId = R.drawable.time, - sortBy = ArtistSortBy.DateAdded + targetSortBy = ArtistSortBy.DateAdded ) Spacer( @@ -125,7 +140,7 @@ fun HomeArtistList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -134,7 +149,7 @@ fun HomeArtistList( } items( - items = viewModel.items, + items = items, key = Artist::id ) { artist -> Column( 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 deleted file mode 100644 index e733957..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt +++ /dev/null @@ -1,67 +0,0 @@ -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/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 3003383..936ed92 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 @@ -35,7 +35,6 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R @@ -44,6 +43,7 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -51,11 +51,14 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.views.BuiltInPlaylistItem import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.playlistSortByKey +import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference @ExperimentalFoundationApi @Composable fun HomePlaylistList( - viewModel: HomePlaylistListViewModel = viewModel(), onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit, onPlaylistClicked: (Playlist) -> Unit, ) { @@ -79,8 +82,18 @@ fun HomePlaylistList( ) } + var sortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded) + var sortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.playlistPreviews(sortBy, sortOrder) }, + stateSaver = PlaylistPreviewListSaver, + key1 = sortBy, + key2 = sortOrder + ) + val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -105,14 +118,14 @@ fun HomePlaylistList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: PlaylistSortBy + targetSortBy: PlaylistSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -136,17 +149,17 @@ fun HomePlaylistList( Item( iconId = R.drawable.medical, - sortBy = PlaylistSortBy.SongCount + targetSortBy = PlaylistSortBy.SongCount ) Item( iconId = R.drawable.text, - sortBy = PlaylistSortBy.Name + targetSortBy = PlaylistSortBy.Name ) Item( iconId = R.drawable.time, - sortBy = PlaylistSortBy.DateAdded + targetSortBy = PlaylistSortBy.DateAdded ) Spacer( @@ -159,7 +172,7 @@ fun HomePlaylistList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -197,7 +210,7 @@ fun HomePlaylistList( } items( - items = viewModel.items, + items = items, key = { it.playlist.id } ) { playlistPreview -> PlaylistPreviewItem( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt deleted file mode 100644 index 257ef63..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt +++ /dev/null @@ -1,70 +0,0 @@ -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.PlaylistSortBy -import it.vfsfitvnm.vimusic.enums.SortOrder -import it.vfsfitvnm.vimusic.models.PlaylistPreview -import it.vfsfitvnm.vimusic.utils.getEnum -import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf -import it.vfsfitvnm.vimusic.utils.playlistSortByKey -import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey -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 HomePlaylistListViewModel(application: Application) : AndroidViewModel(application) { - var items by mutableStateOf(emptyList()) - private set - - var sortBy by mutableStatePreferenceOf( - preferences.getEnum( - playlistSortByKey, - PlaylistSortBy.DateAdded - ) - ) { - preferences.edit { putEnum(playlistSortByKey, it) } - collectItems(sortBy = it) - } - - var sortOrder by mutableStatePreferenceOf( - preferences.getEnum( - playlistSortOrderKey, - SortOrder.Ascending - ) - ) { - preferences.edit { putEnum(playlistSortOrderKey, it) } - collectItems(sortOrder = it) - } - - private var job: Job? = null - - private val preferences: SharedPreferences - get() = getApplication().preferences - - init { - collectItems() - } - - private fun collectItems( - sortBy: PlaylistSortBy = this.sortBy, - sortOrder: SortOrder = this.sortOrder - ) { - job?.cancel() - job = viewModelScope.launch { - Database.playlistPreviews(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { - items = it - } - } - } -} 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 cb64513..f77f25a 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 @@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -32,7 +33,7 @@ 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 it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R @@ -40,6 +41,7 @@ import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -50,21 +52,55 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.songSortByKey +import it.vfsfitvnm.vimusic.utils.songSortOrderKey @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun HomeSongList( - viewModel: HomeSongListViewModel = viewModel() -) { +fun HomeSongList() { + println("[${System.currentTimeMillis()}] HomeSongList") val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val thumbnailSize = Dimensions.thumbnails.song.px + var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded) + var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.songs(sortBy, sortOrder) }, + stateSaver = DetailedSongListSaver, + key1 = sortBy, + key2 = sortOrder + ) + +// var items by rememberSaveable(stateSaver = DetailedSongListSaver) { +// mutableStateOf(emptyList()) +// } +// +// var hasToRecollect by rememberSaveable(sortBy, sortOrder) { +// println("hasToRecollect: $sortBy, $sortOrder") +// mutableStateOf(true) +// } +// +// LaunchedEffect(sortBy, sortOrder) { +// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") +// Database.songs(sortBy, sortOrder) +// .flowOn(Dispatchers.IO) +// .drop(if (hasToRecollect) 0 else 1) +// .collect { +// hasToRecollect = false +// println("[${System.currentTimeMillis()}] collecting... ") +// items = it +// } +// } + val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -74,6 +110,8 @@ fun HomeSongList( .background(colorPalette.background0) .fillMaxSize() ) { +// println("[${System.currentTimeMillis()}] LazyColumn") + item( key = "header", contentType = 0 @@ -82,14 +120,14 @@ fun HomeSongList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: SongSortBy + targetSortBy: SongSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -97,17 +135,17 @@ fun HomeSongList( Item( iconId = R.drawable.trending, - sortBy = SongSortBy.PlayTime + targetSortBy = SongSortBy.PlayTime ) Item( iconId = R.drawable.text, - sortBy = SongSortBy.Title + targetSortBy = SongSortBy.Title ) Item( iconId = R.drawable.time, - sortBy = SongSortBy.DateAdded + targetSortBy = SongSortBy.DateAdded ) Spacer( @@ -120,7 +158,7 @@ fun HomeSongList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -129,25 +167,24 @@ fun HomeSongList( } itemsIndexed( - items = viewModel.items, + items = items, key = { _, song -> song.id } ) { index, song -> SongItem( song = song, thumbnailSize = thumbnailSize, onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - viewModel.items.map(DetailedSong::asMediaItem), - index - ) + items.map(DetailedSong::asMediaItem)?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(mediaItems, index) + } }, menuContent = { InHistoryMediaItemMenu(song = song) }, onThumbnailContent = { AnimatedVisibility( - visible = viewModel.sortBy == SongSortBy.PlayTime, + visible = sortBy == SongSortBy.PlayTime, enter = fadeIn(), exit = fadeOut(), modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt deleted file mode 100644 index 596038f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt +++ /dev/null @@ -1,67 +0,0 @@ -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.SongSortBy -import it.vfsfitvnm.vimusic.enums.SortOrder -import it.vfsfitvnm.vimusic.models.DetailedSong -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 it.vfsfitvnm.vimusic.utils.songSortByKey -import it.vfsfitvnm.vimusic.utils.songSortOrderKey -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch - -class HomeSongListViewModel(application: Application) : AndroidViewModel(application) { - var items by mutableStateOf(emptyList()) - private set - - var sortBy by mutableStatePreferenceOf( - preferences.getEnum( - songSortByKey, - SongSortBy.DateAdded - ) - ) { - preferences.edit { putEnum(songSortByKey, it) } - collectItems(sortBy = it) - } - - var sortOrder by mutableStatePreferenceOf( - preferences.getEnum( - songSortOrderKey, - SortOrder.Ascending - ) - ) { - preferences.edit { putEnum(songSortOrderKey, it) } - collectItems(sortOrder = it) - } - - private var job: Job? = null - - private val preferences: SharedPreferences - get() = getApplication().preferences - - init { - collectItems() - } - - private fun collectItems(sortBy: SongSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) { - job?.cancel() - job = viewModelScope.launch { - Database.songs(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { - items = it - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index ec87fdb..243d5b2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -25,12 +26,11 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -41,6 +41,7 @@ import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableListState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint @@ -49,19 +50,19 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint @Composable fun LocalSongSearch( textFieldValue: TextFieldValue, - onTextFieldValueChanged: (TextFieldValue) -> Unit, - viewModel: LocalSongSearchViewModel = viewModel( - key = textFieldValue.text, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return LocalSongSearchViewModel(textFieldValue.text) as T - } - } - ) + onTextFieldValueChanged: (TextFieldValue) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current + + val items by produceSaveableListState( + flowProvider = { + Database.search("%${textFieldValue.text}%") + }, + stateSaver = DetailedSongListSaver, + key1 = textFieldValue.text + ) + val thumbnailSize = Dimensions.thumbnails.song.px LazyColumn( @@ -122,7 +123,7 @@ fun LocalSongSearch( } items( - items = viewModel.items, + items = items, key = DetailedSong::id, ) { song -> SongItem( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt deleted file mode 100644 index 7735846..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.search - -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.DetailedSong -import kotlinx.coroutines.launch - -class LocalSongSearchViewModel(text: String) : ViewModel() { - var items by mutableStateOf(emptyList()) - private set - - init { - if (text.isNotEmpty()) { - viewModelScope.launch { - Database.search("%$text%").collect { - items = it - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 63db5f4..b690e50 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,21 +40,24 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver +import it.vfsfitvnm.vimusic.savers.StringListResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged @Composable fun OnlineSearch( @@ -61,19 +65,30 @@ fun OnlineSearch( onTextFieldValueChanged: (TextFieldValue) -> Unit, isOpenableUrl: Boolean, onSearch: (String) -> Unit, - onUri: () -> Unit, - viewModel: OnlineSearchViewModel = viewModel( - key = textFieldValue.text, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return OnlineSearchViewModel(textFieldValue.text) as T - } - } - ) + onUri: () -> Unit ) { val (colorPalette, typography) = LocalAppearance.current + val history by produceSaveableListState( + flowProvider = { + Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new -> + old.size == new.size + } + }, + stateSaver = SearchQueryListSaver, + key1 = textFieldValue.text + ) + + val suggestionsResult by produceSaveableState( + initialValue = null, + stateSaver = StringListResultSaver, + key1 = textFieldValue.text + ) { + if (textFieldValue.text.isNotEmpty()) { + value = YouTube.getSearchSuggestions(textFieldValue.text) + } + } + val timeIconPainter = painterResource(R.drawable.time) val closeIconPainter = painterResource(R.drawable.close) val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward) @@ -173,7 +188,7 @@ fun OnlineSearch( } items( - items = viewModel.history, + items = history, key = SearchQuery::id ) { searchQuery -> Row( @@ -241,7 +256,7 @@ fun OnlineSearch( } } - viewModel.suggestionsResult?.getOrNull()?.let { suggestions -> + suggestionsResult?.getOrNull()?.let { suggestions -> items(items = suggestions) { suggestion -> Row( verticalAlignment = Alignment.CenterVertically, @@ -288,7 +303,7 @@ fun OnlineSearch( ) } } - } ?: viewModel.suggestionsResult?.exceptionOrNull()?.let { throwable -> + } ?: suggestionsResult?.exceptionOrNull()?.let { throwable -> item { LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {} } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt deleted file mode 100644 index d96eb7f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt +++ /dev/null @@ -1,36 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.search - -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.SearchQuery -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.launch - -class OnlineSearchViewModel(text: String) : ViewModel() { - var history by mutableStateOf(emptyList()) - private set - - var suggestionsResult by mutableStateOf?>?>(null) - private set - - init { - viewModelScope.launch { - Database.queries("%$text%").distinctUntilChanged { old, new -> - old.size == new.size - }.collect { - history = it - } - } - - if (text.isNotEmpty()) { - viewModelScope.launch { - suggestionsResult = YouTube.getSearchSuggestions(text) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index d718e25..0750a88 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -9,36 +9,59 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.savers.ListSaver +import it.vfsfitvnm.vimusic.savers.StringResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError +import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableState import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @ExperimentalAnimationApi @Composable -inline fun ItemSearchResult( +inline fun SearchResult( query: String, filter: String, + stateSaver: ListSaver>, crossinline onSearchAgain: () -> Unit, - viewModel: SearchResultViewModel = viewModel( - key = query + filter, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return SearchResultViewModel(query, filter) as T - } - } - ), - crossinline itemContent: @Composable LazyItemScope.(I) -> Unit, + crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, noinline itemShimmer: @Composable BoxScope.() -> Unit, ) { + var items by rememberSaveable(query, filter, stateSaver = stateSaver) { + mutableStateOf(listOf()) + } + + val (continuationResultState, fetch) = produceSaveableRelaunchableState( + initialValue = null, + stateSaver = StringResultSaver, + key1 = query, + key2 = filter + ) { + val token = value?.getOrNull() + + value = null + + value = withContext(Dispatchers.IO) { + YouTube.search(query, filter, token) + }?.map { searchResult -> + @Suppress("UNCHECKED_CAST") + items = items.plus(searchResult.items as List).distinctBy(YouTube.Item::key) + searchResult.continuation + } + } + + val continuationResult by continuationResultState + LazyColumn( contentPadding = LocalPlayerAwarePaddingValues.current, modifier = Modifier @@ -60,27 +83,27 @@ inline fun ItemSearchResult( } items( - items = viewModel.items, + items = items, key = { it.key!! }, itemContent = itemContent ) - viewModel.continuationResult?.getOrNull()?.let { - if (viewModel.items.isNotEmpty()) { + continuationResult?.getOrNull()?.let { + if (items.isNotEmpty()) { item { - SideEffect(viewModel::fetch) + SideEffect(fetch) } } - } ?: viewModel.continuationResult?.exceptionOrNull()?.let { throwable -> + } ?: continuationResult?.exceptionOrNull()?.let { throwable -> item { SearchResultLoadingOrError( errorMessage = throwable.javaClass.canonicalName, - onRetry = viewModel::fetch, + onRetry = fetch, shimmerContent = {} ) } - } ?: viewModel.continuationResult?.let { - if (viewModel.items.isEmpty()) { + } ?: continuationResult?.let { + if (items.isEmpty()) { item { TextCard(icon = R.drawable.sad) { Title(text = "No results found") @@ -90,7 +113,7 @@ inline fun ItemSearchResult( } } ?: item(key = "loading") { SearchResultLoadingOrError( - itemCount = if (viewModel.items.isEmpty()) 8 else 3, + itemCount = if (items.isEmpty()) 8 else 3, shimmerContent = itemShimmer ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index 3252377..ec8f8ab 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -13,6 +13,11 @@ import androidx.compose.ui.unit.dp import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver +import it.vfsfitvnm.vimusic.savers.YouTubeArtistListSaver +import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver +import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver +import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.PlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute @@ -85,10 +90,11 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, onSearchAgain = onSearchAgain, + stateSaver = YouTubeSongListSaver, itemContent = { song -> SmallSongItem( song = song, @@ -110,9 +116,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubeAlbumListSaver, onSearchAgain = onSearchAgain, itemContent = { album -> AlbumItem( @@ -138,9 +145,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 64.dp val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubeArtistListSaver, onSearchAgain = onSearchAgain, itemContent = { artist -> ArtistItem( @@ -165,9 +173,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailHeightDp = 72.dp val thumbnailWidthDp = 128.dp - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubeVideoListSaver, onSearchAgain = onSearchAgain, itemContent = { video -> VideoItem( @@ -194,9 +203,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubePlaylistListSaver, onSearchAgain = onSearchAgain, itemContent = { playlist -> PlaylistItem( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt deleted file mode 100644 index bfbfaab..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.searchresult - -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.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class SearchResultViewModel( - private val query: String, - private val filter: String -) : ViewModel() { - var items by mutableStateOf(listOf()) - - var continuationResult by mutableStateOf?>(null) - - private var job: Job? = null - - init { - fetch() - } - - fun fetch() { - job?.cancel() - - viewModelScope.launch { - val token = continuationResult?.getOrNull() - - continuationResult = null - - continuationResult = withContext(Dispatchers.IO) { - YouTube.search(query, filter, token) - }?.map { searchResult -> - @Suppress("UNCHECKED_CAST") - items = items.plus(searchResult.items as List).distinctBy(YouTube.Item::key) - searchResult.continuation - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index 467eb79..f6f2126 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -83,7 +83,7 @@ fun SmallSongItem( SongItem( thumbnailModel = song.thumbnail?.size(thumbnailSizePx), title = song.info.name, - authors = song.authors.joinToString("") { it.name }, + authors = song.authors?.joinToString("") { it.name } ?: "", durationText = song.durationText, onClick = onClick, menuContent = { @@ -158,13 +158,13 @@ fun VideoItem( ) BasicText( - text = video.authors.joinToString("") { it.name }, + text = video.authors?.joinToString("") { it.name } ?: "", style = typography.xs.semiBold.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - video.views.firstOrNull()?.name?.let { viewsText -> + video.viewsText?.let { viewsText -> BasicText( text = viewsText, style = typography.xxs.medium.secondary, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt new file mode 100644 index 0000000..f765c76 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt @@ -0,0 +1,102 @@ +package it.vfsfitvnm.vimusic.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import it.vfsfitvnm.vimusic.savers.ListSaver +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.flowOn + + +@Composable +fun produceSaveableListState( + flowProvider: () -> Flow>, + stateSaver: ListSaver>, +): State> { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(emptyList()) + } + + var hasToRecollect by rememberSaveable { + mutableStateOf(true) + } + + LaunchedEffect(Unit) { + flowProvider() + .flowOn(Dispatchers.IO) + .drop(if (hasToRecollect) 0 else 1) + .collect { + hasToRecollect = false + state.value = it + } + } + + return state +} + +@Composable +fun produceSaveableListState( + flowProvider: () -> Flow>, + stateSaver: ListSaver>, + key1: Any?, +): State> { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(emptyList()) + } + + var hasToRecollect by rememberSaveable(key1) { +// println("hasToRecollect: $sortBy, $sortOrder") + mutableStateOf(true) + } + + LaunchedEffect(key1) { +// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") + flowProvider() + .flowOn(Dispatchers.IO) + .drop(if (hasToRecollect) 0 else 1) + .collect { + hasToRecollect = false +// println("[${System.currentTimeMillis()}] collecting... ") + state.value = it + } + } + + return state +} + +@Composable +fun produceSaveableListState( + flowProvider: () -> Flow>, + stateSaver: ListSaver>, + key1: Any?, + key2: Any?, +): State> { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(emptyList()) + } + +// var hasToRecollect by rememberSaveable(key1, key2) { +//// println("hasToRecollect: $sortBy, $sortOrder") +// mutableStateOf(true) +// } + + LaunchedEffect(key1, key2) { +// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") + flowProvider() + .flowOn(Dispatchers.IO) +// .drop(if (hasToRecollect) 0 else 1) + .collect { +// hasToRecollect = false +// println("[${System.currentTimeMillis()}] collecting... ") + state.value = it + } + } + + return state +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt new file mode 100644 index 0000000..720ddac --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt @@ -0,0 +1,118 @@ +package it.vfsfitvnm.vimusic.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.ProduceStateScope +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import kotlin.coroutines.CoroutineContext +import kotlin.experimental.ExperimentalTypeInference +import kotlinx.coroutines.suspendCancellableCoroutine + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableState( + initialValue: T, + stateSaver: Saver, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable { mutableStateOf(true) } + + LaunchedEffect(Unit) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + return result +} + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableState( + initialValue: T, + stateSaver: Saver, + key1: Any?, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable(key1) { mutableStateOf(true) } + + LaunchedEffect(key1) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + return result +} + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableState( + initialValue: T, + stateSaver: Saver, + key1: Any?, + key2: Any?, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) } + + LaunchedEffect(Unit) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + + return result +} + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableRelaunchableState( + initialValue: T, + stateSaver: Saver, + key1: Any?, + key2: Any?, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): Pair, () -> Unit> { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) } + + val relaunchableEffect = relaunchableEffect(key1, key2) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + + return result to { + hasToFetch = true + relaunchableEffect() + } +} + +private class ProduceSaveableStateScope( + state: MutableState, + override val coroutineContext: CoroutineContext +) : ProduceStateScope, MutableState by state { + override suspend fun awaitDispose(onDispose: () -> Unit): Nothing { + try { + suspendCancellableCoroutine { } + } finally { + onDispose() + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 269668c..82ea165 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -28,7 +28,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem .setMediaMetadata( MediaMetadata.Builder() .setTitle(info.name) - .setArtist(authors.joinToString("") { it.name }) + .setArtist(authors?.joinToString("") { it.name }) .setAlbumTitle(album?.name) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( @@ -36,8 +36,8 @@ val YouTube.Item.Song.asMediaItem: MediaItem "videoId" to info.endpoint!!.videoId, "albumId" to album?.endpoint?.browseId, "durationText" to durationText, - "artistNames" to authors.filter { it.endpoint != null }.map { it.name }, - "artistIds" to authors.mapNotNull { it.endpoint?.browseId }, + "artistNames" to authors?.filter { it.endpoint != null }?.map { it.name }, + "artistIds" to authors?.mapNotNull { it.endpoint?.browseId }, ) ) .build() @@ -52,14 +52,14 @@ val YouTube.Item.Video.asMediaItem: MediaItem .setMediaMetadata( MediaMetadata.Builder() .setTitle(info.name) - .setArtist(authors.joinToString("") { it.name }) + .setArtist(authors?.joinToString("") { it.name }) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( bundleOf( "videoId" to info.endpoint!!.videoId, "durationText" to durationText, - "artistNames" to if (isOfficialMusicVideo) authors.filter { it.endpoint != null }.map { it.name } else null, - "artistIds" to if (isOfficialMusicVideo) authors.mapNotNull { it.endpoint?.browseId } else null, + "artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.map { it.name } else null, + "artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null, ) ) .build() diff --git a/settings.gradle.kts b/settings.gradle.kts index ececf2e..176589f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,6 @@ dependencyResolutionManagement { library("compose-shimmer", "com.valentinilk.shimmer", "compose-shimmer").version("1.0.3") - library("compose-viewmodel", "androidx.lifecycle", "lifecycle-viewmodel-compose").version("2.6.0-alpha02") library("compose-activity", "androidx.activity", "activity-compose").version("1.5.1") library("compose-coil", "io.coil-kt", "coil-compose").version("2.2.1") diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index 7eec479..40ec4bf 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -181,7 +181,7 @@ object YouTube { data class Song( val info: Info, - val authors: List>, + val authors: List>?, val album: Info?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? @@ -231,8 +231,8 @@ object YouTube { data class Video( val info: Info, - val authors: List>, - val views: List>, + val authors: List>?, + val viewsText: String?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { @@ -263,14 +263,14 @@ object YouTube { info = Info.from(mainRuns.first()), authors = otherRuns .getOrNull(otherRuns.lastIndex - 2) - ?.map(Info.Companion::from) - ?: emptyList(), - views = otherRuns + ?.map(Info.Companion::from), + viewsText = otherRuns .getOrNull(otherRuns.lastIndex - 1) - ?.map(Info.Companion::from) ?: emptyList(), + ?.firstOrNull() + ?.text, durationText = otherRuns .getOrNull(otherRuns.lastIndex) - ?.first() + ?.firstOrNull() ?.text, thumbnail = content .thumbnail