Drop ViewModel

This commit is contained in:
vfsfitvnm 2022-09-26 14:52:39 +02:00
parent 29b4a8f5da
commit f981725062
69 changed files with 1269 additions and 2174 deletions

View file

@ -84,7 +84,6 @@ dependencies {
implementation(libs.compose.ripple)
implementation(libs.compose.shimmer)
implementation(libs.compose.coil)
implementation(libs.compose.viewmodel)
implementation(libs.palette)

View file

@ -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')"
]
}
}

View file

@ -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')"
]
}
}

View file

@ -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')"
]
}
}

View file

@ -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<AlbumWithSongs?>
fun album(id: String): Flow<Album?>
@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<List<DetailedSong>>
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC")
fun albumsByTitleAsc(): Flow<List<Album>>
@ -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)

View file

@ -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<DetailedSong>
)

View file

@ -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
)

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val AlbumListSaver = ListSaver.of(AlbumSaver)

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val AlbumResultSaver = ResultSaver.of(AlbumSaver)

View file

@ -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<Album, List<Any?>> {
override fun SaverScope.save(value: Album): List<Any?> = listOf(
value.id,
value.title,
value.thumbnailUrl,
value.year,
value.authorsText,
value.shareUrl,
value.timestamp,
value.bookmarkedAt,
)
override fun restore(value: List<Any?>): 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?,
)
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val ArtistListSaver = ListSaver.of(ArtistSaver)

View file

@ -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<Artist, List<Any?>> {
override fun SaverScope.save(value: Artist): List<Any?> = 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<Any?>): 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?,
)
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val DetailedSongListSaver = ListSaver.of(DetailedSongSaver)

View file

@ -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<DetailedSong, List<Any?>> {
override fun SaverScope.save(value: DetailedSong): List<Any?> =
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<Any?>): 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<List<String>>)
) else null
}
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val InfoListSaver = ListSaver.of(InfoSaver)

View file

@ -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<Info, List<String>> {
override fun SaverScope.save(value: Info): List<String> = listOf(value.id, value.name)
override fun restore(value: List<String>): Info? {
return if (value.size == 2) Info(
id = value[0],
name = value[1],
) else null
}
}

View file

@ -0,0 +1,20 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
companion object {
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>): ListSaver<Original, Saveable> {
return object : ListSaver<Original, Saveable> {
override fun restore(value: List<Saveable>): List<Original> {
return value.mapNotNull(saver::restore)
}
override fun SaverScope.save(value: List<Original>): List<Saveable> {
return with(saver) { value.mapNotNull { save(it) } }
}
}
}
}
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val PlaylistPreviewListSaver = ListSaver.of(PlaylistPreviewSaver)

View file

@ -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<PlaylistPreview, List<Any?>> {
override fun SaverScope.save(value: PlaylistPreview): List<Any> {
return listOf(
with(PlaylistSaver) { save(value.playlist) },
value.songCount,
)
}
override fun restore(value: List<Any?>): PlaylistPreview? {
return if (value.size == 2) PlaylistPreview(
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
songCount = value[1] as Int,
) else null
}
}

View file

@ -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<Playlist, List<Any?>> {
override fun SaverScope.save(value: Playlist): List<Any?> = listOf(
value.id,
value.name,
value.browseId,
)
override fun restore(value: List<Any?>): Playlist = Playlist(
id = value[0] as Long,
name = value[1] as String,
browseId = value[2] as String?,
)
}

View file

@ -0,0 +1,18 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
interface ResultSaver<Original, Saveable> : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
companion object {
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>) =
object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
override fun restore(value: Pair<Saveable?, Throwable?>) =
value.first?.let(saver::restore)?.let(Result.Companion::success)
?: value.second?.let(Result.Companion::failure)
override fun SaverScope.save(value: Result<Original>?) =
with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull()
}
}
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val SearchQueryListSaver = ListSaver.of(SearchQuerySaver)

View file

@ -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<SearchQuery, List<Any?>> {
override fun SaverScope.save(value: SearchQuery): List<Any?> = listOf(
value.id,
value.query,
)
override fun restore(value: List<Any?>) = SearchQuery(
id = value[0] as Long,
query = value[1] as String
)
}

View file

@ -0,0 +1,5 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.autoSaver
val StringListResultSaver = ResultSaver.of(autoSaver<List<String>?>())

View file

@ -0,0 +1,5 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.autoSaver
val StringResultSaver = ResultSaver.of(autoSaver<String?>())

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeAlbumListSaver = ListSaver.of(YouTubeAlbumSaver)

View file

@ -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<YouTube.Item.Album, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Album): List<Any?> = 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<Any?>) = YouTube.Item.Album(
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
year = value[2] as String?,
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeArtistListSaver = ListSaver.of(YouTubeArtistSaver)

View file

@ -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<YouTube.Item.Artist, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
with(YouTubeBrowseInfoSaver) { save(value.info) },
value.subscribersCountText,
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Item.Artist(
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
subscribersCountText = value[1] as String?,
thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -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<NavigationEndpoint.Endpoint.Browse, List<Any?>> {
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf(
value.browseId,
value.params
)
override fun restore(value: List<Any?>) = NavigationEndpoint.Endpoint.Browse(
browseId = value[0] as String,
params = value[1] as String?,
browseEndpointContextSupportedConfigs = null
)
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeBrowseInfoListSaver = ListSaver.of(YouTubeBrowseInfoSaver)

View file

@ -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<YouTube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
value.name,
with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Info(
name = value[0] as String,
endpoint = (value[1] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore)
)
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val YouTubePlaylistListSaver = ListSaver.of(YouTubePlaylistSaver)

View file

@ -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<YouTube.Item.Playlist, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Playlist): List<Any?> = 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<Any?>) = YouTube.Item.Playlist(
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
channel = (value[1] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
songCount = value[2] as Int?,
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeSongListSaver = ListSaver.of(YouTubeSongSaver)

View file

@ -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<YouTube.Item.Song, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Song): List<Any?> = 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<Any?>) = YouTube.Item.Song(
info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
album = (value[2] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -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<ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail, List<Any?>> {
override fun SaverScope.save(value: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail) = listOf(
value.url,
value.width,
value.height
)
override fun restore(value: List<Any?>) = ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail(
url = value[0] as String,
width = value[1] as Int,
height = value[2] as Int?,
)
}

View file

@ -0,0 +1,3 @@
package it.vfsfitvnm.vimusic.savers
val YouTubeVideoListSaver = ListSaver.of(YouTubeVideoSaver)

View file

@ -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<YouTube.Item.Video, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Video): List<Any?> = 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<Any?>) = YouTube.Item.Video(
info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
viewsText = value[2] as String?,
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View file

@ -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<NavigationEndpoint.Endpoint.Watch, List<Any?>> {
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf(
value.params,
value.playlistId,
value.videoId,
value.index,
value.playlistSetVideoId,
)
override fun restore(value: List<Any?>) = 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
)
}

View file

@ -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<YouTube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
value.name,
with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Info(
name = value[0] as String,
endpoint = (value[1] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore)
)
}

View file

@ -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<String?>("albumRoute")
val artistRoute = Route1<String?>("artistRoute")

View file

@ -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<Album>?,
browseId: String,
viewModel: AlbumOverviewViewModel = viewModel(
key = browseId,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): 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)

View file

@ -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<Result<AlbumWithSongs?>?>(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)
}
}
}
}
}

View file

@ -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,
)
}
}
}

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): 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()
// }
// }
// }
// }
// }
}

View file

@ -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<Result<AlbumWithSongs?>?>(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)
}
}
}
}
}

View file

@ -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)

View file

@ -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(

View file

@ -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<Album>())
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<Application>().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
}
}
}
}

View file

@ -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(

View file

@ -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<Artist>())
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<Application>().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
}
}
}
}

View file

@ -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(

View file

@ -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<PlaylistPreview>())
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<Application>().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
}
}
}
}

View file

@ -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

View file

@ -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<DetailedSong>())
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<Application>().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
}
}
}
}

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): 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(

View file

@ -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<DetailedSong>())
private set
init {
if (text.isNotEmpty()) {
viewModelScope.launch {
Database.search("%$text%").collect {
items = it
}
}
}
}
}

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): 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) {}
}

View file

@ -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<SearchQuery>())
private set
var suggestionsResult by mutableStateOf<Result<List<String>?>?>(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)
}
}
}
}

View file

@ -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 <I : YouTube.Item> ItemSearchResult(
inline fun <T : YouTube.Item> SearchResult(
query: String,
filter: String,
stateSaver: ListSaver<T, List<Any?>>,
crossinline onSearchAgain: () -> Unit,
viewModel: SearchResultViewModel<I> = viewModel(
key = query + filter,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return SearchResultViewModel<I>(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<T>).distinctBy(YouTube.Item::key)
searchResult.continuation
}
}
val continuationResult by continuationResultState
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
@ -60,27 +83,27 @@ inline fun <I : YouTube.Item> 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 <I : YouTube.Item> ItemSearchResult(
}
} ?: item(key = "loading") {
SearchResultLoadingOrError(
itemCount = if (viewModel.items.isEmpty()) 8 else 3,
itemCount = if (items.isEmpty()) 8 else 3,
shimmerContent = itemShimmer
)
}

View file

@ -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<YouTube.Item.Song>(
SearchResult<YouTube.Item.Song>(
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<YouTube.Item.Album>(
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<YouTube.Item.Artist>(
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<YouTube.Item.Video>(
SearchResult<YouTube.Item.Video>(
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<YouTube.Item.Playlist>(
SearchResult<YouTube.Item.Playlist>(
query = query,
filter = searchFilter,
stateSaver = YouTubePlaylistListSaver,
onSearchAgain = onSearchAgain,
itemContent = { playlist ->
PlaylistItem(

View file

@ -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<T : YouTube.Item>(
private val query: String,
private val filter: String
) : ViewModel() {
var items by mutableStateOf(listOf<T>())
var continuationResult by mutableStateOf<Result<String?>?>(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<T>).distinctBy(YouTube.Item::key)
searchResult.continuation
}
}
}
}

View file

@ -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,

View file

@ -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 <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
): State<List<T>> {
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 <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
key1: Any?,
): State<List<T>> {
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 <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
key1: Any?,
key2: Any?,
): State<List<T>> {
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
}

View file

@ -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 <T> produceSaveableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
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 <T> produceSaveableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
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 <T> produceSaveableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
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 <T> produceSaveableRelaunchableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): Pair<State<T>, () -> 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<T>(
state: MutableState<T>,
override val coroutineContext: CoroutineContext
) : ProduceStateScope<T>, MutableState<T> by state {
override suspend fun awaitDispose(onDispose: () -> Unit): Nothing {
try {
suspendCancellableCoroutine<Nothing> { }
} finally {
onDispose()
}
}
}

View file

@ -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()

View file

@ -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")

View file

@ -181,7 +181,7 @@ object YouTube {
data class Song(
val info: Info<NavigationEndpoint.Endpoint.Watch>,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
val durationText: String?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
@ -231,8 +231,8 @@ object YouTube {
data class Video(
val info: Info<NavigationEndpoint.Endpoint.Watch>,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>,
val views: List<Info<NavigationEndpoint.Endpoint.Browse>>,
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
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