646
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/21.json
Normal file
646
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/21.json
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 21,
|
||||||
|
"identityHash": "5afda34f61cc45ecd6102a7285ec92d2",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Song",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `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": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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, `thumbnailUrl` TEXT, `info` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "info",
|
||||||
|
"columnName": "info",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Event",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playTime",
|
||||||
|
"columnName": "playTime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Event_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Event_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '5afda34f61cc45ecd6102a7285ec92d2')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import androidx.media3.common.MediaItem
|
|||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
|
import androidx.room.DeleteColumn
|
||||||
import androidx.room.DeleteTable
|
import androidx.room.DeleteTable
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
@@ -39,6 +40,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong
|
|||||||
import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
|
import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
|
||||||
import it.vfsfitvnm.vimusic.models.Event
|
import it.vfsfitvnm.vimusic.models.Event
|
||||||
import it.vfsfitvnm.vimusic.models.Format
|
import it.vfsfitvnm.vimusic.models.Format
|
||||||
|
import it.vfsfitvnm.vimusic.models.PartialArtist
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
||||||
@@ -150,16 +152,16 @@ interface Database {
|
|||||||
@Query("SELECT * FROM Artist WHERE id = :id")
|
@Query("SELECT * FROM Artist WHERE id = :id")
|
||||||
fun artist(id: String): Flow<Artist?>
|
fun artist(id: String): Flow<Artist?>
|
||||||
|
|
||||||
@Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY name DESC")
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY name DESC")
|
||||||
fun artistsByNameDesc(): Flow<List<Artist>>
|
fun artistsByNameDesc(): Flow<List<Artist>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY name ASC")
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY name ASC")
|
||||||
fun artistsByNameAsc(): Flow<List<Artist>>
|
fun artistsByNameAsc(): Flow<List<Artist>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY ROWID DESC")
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID DESC")
|
||||||
fun artistsByRowIdDesc(): Flow<List<Artist>>
|
fun artistsByRowIdDesc(): Flow<List<Artist>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY ROWID ASC")
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID ASC")
|
||||||
fun artistsByRowIdAsc(): Flow<List<Artist>>
|
fun artistsByRowIdAsc(): Flow<List<Artist>>
|
||||||
|
|
||||||
fun artists(sortBy: ArtistSortBy, sortOrder: SortOrder): Flow<List<Artist>> {
|
fun artists(sortBy: ArtistSortBy, sortOrder: SortOrder): Flow<List<Artist>> {
|
||||||
@@ -378,7 +380,7 @@ interface Database {
|
|||||||
name = artistName,
|
name = artistName,
|
||||||
thumbnailUrl = null,
|
thumbnailUrl = null,
|
||||||
info = null,
|
info = null,
|
||||||
timestamp = null,
|
timestamp = null
|
||||||
).also(::insert)
|
).also(::insert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,6 +421,9 @@ interface Database {
|
|||||||
@Upsert
|
@Upsert
|
||||||
fun upsert(artist: Artist)
|
fun upsert(artist: Artist)
|
||||||
|
|
||||||
|
@Upsert(Artist::class)
|
||||||
|
fun upsert(artist: PartialArtist)
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
fun delete(searchQuery: SearchQuery)
|
fun delete(searchQuery: SearchQuery)
|
||||||
|
|
||||||
@@ -449,7 +454,7 @@ interface Database {
|
|||||||
views = [
|
views = [
|
||||||
SortedSongPlaylistMap::class
|
SortedSongPlaylistMap::class
|
||||||
],
|
],
|
||||||
version = 20,
|
version = 21,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -468,6 +473,7 @@ interface Database {
|
|||||||
AutoMigration(from = 17, to = 18),
|
AutoMigration(from = 17, to = 18),
|
||||||
AutoMigration(from = 18, to = 19),
|
AutoMigration(from = 18, to = 19),
|
||||||
AutoMigration(from = 19, to = 20),
|
AutoMigration(from = 19, to = 20),
|
||||||
|
AutoMigration(from = 20, to = 21, spec = DatabaseInitializer.From20To21Migration::class),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
@@ -601,6 +607,14 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
|
|||||||
it.execSQL("ALTER TABLE Song_new RENAME TO Song;")
|
it.execSQL("ALTER TABLE Song_new RENAME TO Song;")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteColumn.Entries(
|
||||||
|
DeleteColumn("Artist", "shuffleVideoId"),
|
||||||
|
DeleteColumn("Artist", "shufflePlaylistId"),
|
||||||
|
DeleteColumn("Artist", "radioVideoId"),
|
||||||
|
DeleteColumn("Artist", "radioPlaylistId"),
|
||||||
|
)
|
||||||
|
class From20To21Migration : AutoMigrationSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverters
|
@TypeConverters
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ import androidx.room.PrimaryKey
|
|||||||
@Entity
|
@Entity
|
||||||
data class Artist(
|
data class Artist(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val name: String,
|
val name: String?,
|
||||||
val thumbnailUrl: String?,
|
val thumbnailUrl: String?,
|
||||||
val info: String?,
|
val info: String?,
|
||||||
val shuffleVideoId: String? = null,
|
|
||||||
val shufflePlaylistId: String? = null,
|
|
||||||
val radioVideoId: String? = null,
|
|
||||||
val radioPlaylistId: String? = null,
|
|
||||||
val timestamp: Long?,
|
val timestamp: Long?,
|
||||||
val bookmarkedAt: Long? = null,
|
val bookmarkedAt: Long? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.models
|
||||||
|
|
||||||
|
data class PartialArtist(
|
||||||
|
val id: String,
|
||||||
|
val name: String?,
|
||||||
|
val thumbnailUrl: String?,
|
||||||
|
val info: String?,
|
||||||
|
val timestamp: Long? = null,
|
||||||
|
)
|
||||||
@@ -3,7 +3,6 @@ package it.vfsfitvnm.vimusic.savers
|
|||||||
import androidx.compose.runtime.saveable.Saver
|
import androidx.compose.runtime.saveable.Saver
|
||||||
import androidx.compose.runtime.saveable.SaverScope
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
import it.vfsfitvnm.vimusic.models.Artist
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
|
||||||
|
|
||||||
object ArtistSaver : Saver<Artist, List<Any?>> {
|
object ArtistSaver : Saver<Artist, List<Any?>> {
|
||||||
override fun SaverScope.save(value: Artist): List<Any?> = listOf(
|
override fun SaverScope.save(value: Artist): List<Any?> = listOf(
|
||||||
@@ -11,10 +10,6 @@ object ArtistSaver : Saver<Artist, List<Any?>> {
|
|||||||
value.name,
|
value.name,
|
||||||
value.thumbnailUrl,
|
value.thumbnailUrl,
|
||||||
value.info,
|
value.info,
|
||||||
value.shuffleVideoId,
|
|
||||||
value.shufflePlaylistId,
|
|
||||||
value.radioVideoId,
|
|
||||||
value.radioPlaylistId,
|
|
||||||
value.timestamp,
|
value.timestamp,
|
||||||
value.bookmarkedAt,
|
value.bookmarkedAt,
|
||||||
)
|
)
|
||||||
@@ -24,11 +19,7 @@ object ArtistSaver : Saver<Artist, List<Any?>> {
|
|||||||
name = value[1] as String,
|
name = value[1] as String,
|
||||||
thumbnailUrl = value[2] as String?,
|
thumbnailUrl = value[2] as String?,
|
||||||
info = value[3] as String?,
|
info = value[3] as String?,
|
||||||
shuffleVideoId = value[4] as String?,
|
timestamp = value[4] as Long?,
|
||||||
shufflePlaylistId = value[5] as String?,
|
bookmarkedAt = value[5] as Long?,
|
||||||
radioVideoId = value[6] as String?,
|
|
||||||
radioPlaylistId = value[7] as String?,
|
|
||||||
timestamp = value[8] as Long?,
|
|
||||||
bookmarkedAt = value[9] as Long?,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
|
object YouTubeArtistPageSaver : Saver<YouTube.Artist, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Artist): List<Any?> = listOf(
|
||||||
|
value.name,
|
||||||
|
value.description,
|
||||||
|
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } },
|
||||||
|
value.shuffleEndpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } },
|
||||||
|
value.radioEndpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } },
|
||||||
|
value.songs?.let { with(YouTubeSongListSaver) { save(it) } },
|
||||||
|
value.songsEndpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } },
|
||||||
|
value.albums?.let { with(YouTubeAlbumListSaver) { save(it) } },
|
||||||
|
value.albumsEndpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } },
|
||||||
|
value.singles?.let { with(YouTubeAlbumListSaver) { save(it) } },
|
||||||
|
value.singlesEndpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } },
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Artist(
|
||||||
|
name = value[0] as String?,
|
||||||
|
description = value[1] as String?,
|
||||||
|
thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore),
|
||||||
|
shuffleEndpoint = (value[3] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore),
|
||||||
|
radioEndpoint = (value[4] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore),
|
||||||
|
songs = (value[5] as List<List<Any?>>?)?.let(YouTubeSongListSaver::restore),
|
||||||
|
songsEndpoint = (value[6] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore),
|
||||||
|
albums = (value[7] as List<List<Any?>>?)?.let(YouTubeAlbumListSaver::restore),
|
||||||
|
albumsEndpoint = (value[8] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore),
|
||||||
|
singles = (value[9] as List<List<Any?>>?)?.let(YouTubeAlbumListSaver::restore),
|
||||||
|
singlesEndpoint = (value[10] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,13 +2,14 @@ package it.vfsfitvnm.vimusic.savers
|
|||||||
|
|
||||||
import androidx.compose.runtime.saveable.Saver
|
import androidx.compose.runtime.saveable.Saver
|
||||||
import androidx.compose.runtime.saveable.SaverScope
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeThumbnailSaver.save
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
|
object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
|
||||||
override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
|
override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
|
||||||
value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
|
value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
|
||||||
value.subscribersCountText,
|
value.subscribersCountText,
|
||||||
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun restore(value: List<Any?>) = YouTube.Item.Artist(
|
override fun restore(value: List<Any?>) = YouTube.Item.Artist(
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShimmerHost(content: @Composable ColumnScope.() -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.shimmer()
|
||||||
|
.graphicsLayer(alpha = 0.99f)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
listOf(Color.Black, Color.Transparent)
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.artist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
|
import it.vfsfitvnm.vimusic.savers.ListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
inline fun <T : YouTube.Item> ArtistContent(
|
||||||
|
artist: Artist?,
|
||||||
|
youtubeArtist: YouTube.Artist?,
|
||||||
|
isLoading: Boolean,
|
||||||
|
isError: Boolean,
|
||||||
|
stateSaver: ListSaver<T, List<Any?>>,
|
||||||
|
crossinline itemsProvider: suspend (String?) -> Result<Pair<String?, List<T>?>>?,
|
||||||
|
crossinline bookmarkIconContent: @Composable () -> Unit,
|
||||||
|
crossinline shareIconContent: @Composable () -> Unit,
|
||||||
|
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
|
||||||
|
noinline itemShimmer: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val (_, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
var items by rememberSaveable(stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(listOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLoadingItems by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isErrorItems by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (continuationState, fetch) = produceSaveableRelaunchableOneShotState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = autoSaver<String?>(),
|
||||||
|
youtubeArtist
|
||||||
|
) {
|
||||||
|
if (youtubeArtist == null) return@produceSaveableRelaunchableOneShotState
|
||||||
|
|
||||||
|
println("loading... $value")
|
||||||
|
|
||||||
|
isLoadingItems = true
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
itemsProvider(value)?.onSuccess { (continuation, newItems) ->
|
||||||
|
value = continuation
|
||||||
|
newItems?.let {
|
||||||
|
items = items.plus(it).distinctBy(YouTube.Item::key)
|
||||||
|
}
|
||||||
|
isErrorItems = false
|
||||||
|
isLoadingItems = false
|
||||||
|
}?.onFailure {
|
||||||
|
println("error (2): $it")
|
||||||
|
isErrorItems = true
|
||||||
|
isLoadingItems = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val continuation by continuationState
|
||||||
|
|
||||||
|
when {
|
||||||
|
artist != null -> {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0,
|
||||||
|
) {
|
||||||
|
Header(title = artist.name ?: "Unknown") {
|
||||||
|
bookmarkIconContent()
|
||||||
|
shareIconContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = YouTube.Item::key,
|
||||||
|
itemContent = itemContent
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isError || isErrorItems) {
|
||||||
|
item(key = "error") {
|
||||||
|
BasicText(
|
||||||
|
text = "An error has occurred",
|
||||||
|
style = LocalAppearance.current.typography.s.secondary.center,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item("loading") {
|
||||||
|
val hasMore = continuation != null
|
||||||
|
|
||||||
|
if (hasMore || items.isEmpty()) {
|
||||||
|
ShimmerHost {
|
||||||
|
repeat(if (hasMore) 3 else 8) {
|
||||||
|
itemShimmer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (hasMore && items.isNotEmpty()) {
|
||||||
|
// println("loading again!")
|
||||||
|
// SideEffect(fetch)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isError -> BasicText(
|
||||||
|
text = "An error has occurred",
|
||||||
|
style = LocalAppearance.current.typography.s.secondary.center,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
)
|
||||||
|
isLoading -> ShimmerHost {
|
||||||
|
HeaderPlaceholder()
|
||||||
|
|
||||||
|
repeat(5) {
|
||||||
|
itemShimmer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.artist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
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.Artist
|
||||||
|
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.HeaderPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun ArtistLocalSongsList(
|
||||||
|
browseId: String,
|
||||||
|
artist: Artist?,
|
||||||
|
isLoading: Boolean,
|
||||||
|
isError: Boolean,
|
||||||
|
bookmarkIconContent: @Composable () -> Unit,
|
||||||
|
shareIconContent: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
|
val songs by produceSaveableState(
|
||||||
|
initialValue = emptyList(),
|
||||||
|
stateSaver = DetailedSongListSaver
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.artistSongs(browseId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val songThumbnailSizePx = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
|
BoxWithConstraints {
|
||||||
|
val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth
|
||||||
|
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||||
|
|
||||||
|
when {
|
||||||
|
artist != null -> {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Header(title = artist.name ?: "Unknown") {
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "Enqueue",
|
||||||
|
isEnabled = songs.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
bookmarkIconContent()
|
||||||
|
shareIconContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = songs,
|
||||||
|
key = { _, song -> song.id }
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizePx = songThumbnailSizePx,
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(
|
||||||
|
songs.map(DetailedSong::asMediaItem),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
},
|
||||||
|
menuContent = {
|
||||||
|
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryButton(
|
||||||
|
iconId = R.drawable.shuffle,
|
||||||
|
isEnabled = songs.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(
|
||||||
|
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isError -> Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "An error has occurred.",
|
||||||
|
style = typography.s.secondary.center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isLoading -> ShimmerHost {
|
||||||
|
HeaderPlaceholder()
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,76 +1,373 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.artist
|
package it.vfsfitvnm.vimusic.ui.screens.artist
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
import it.vfsfitvnm.vimusic.ui.views.AlternativeAlbumItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.AlternativeAlbumItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SmallSongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@ExperimentalFoundationApi
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArtistOverview(
|
fun ArtistOverview(
|
||||||
browseId: String,
|
artist: Artist?,
|
||||||
|
youtubeArtist: YouTube.Artist?,
|
||||||
|
isLoading: Boolean,
|
||||||
|
isError: Boolean,
|
||||||
|
onViewAllSongsClick: () -> Unit,
|
||||||
|
onViewAllAlbumsClick: () -> Unit,
|
||||||
|
onViewAllSinglesClick: () -> Unit,
|
||||||
|
onAlbumClick: (String) -> Unit,
|
||||||
|
bookmarkIconContent: @Composable () -> Unit,
|
||||||
|
shareIconContent: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val context = LocalContext.current
|
|
||||||
|
val songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||||
|
val albumThumbnailSizeDp = 108.dp
|
||||||
|
val albumThumbnailSizePx = albumThumbnailSizeDp.px
|
||||||
|
|
||||||
|
val sectionTextModifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(top = 24.dp, bottom = 8.dp)
|
||||||
|
|
||||||
|
BoxWithConstraints {
|
||||||
|
val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth
|
||||||
|
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
artist != null -> {
|
||||||
|
Header(title = artist.name ?: "Unknown") {
|
||||||
|
youtubeArtist?.radioEndpoint?.let { radioEndpoint ->
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "Start radio",
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.playRadio(radioEndpoint)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
bookmarkIconContent()
|
||||||
|
shareIconContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
when {
|
||||||
|
youtubeArtist != null -> {
|
||||||
|
youtubeArtist.songs?.let { songs ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Songs",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
youtubeArtist.songsEndpoint?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "View all",
|
||||||
|
style = typography.xs.secondary,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onViewAllSongsClick
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
songs.forEach { song ->
|
||||||
|
SmallSongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizePx = songThumbnailSizePx,
|
||||||
|
onClick = {
|
||||||
|
val mediaItem = song.asMediaItem
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlay(mediaItem)
|
||||||
|
binder?.setupRadio(
|
||||||
|
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeArtist.albums?.let { albums ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Albums",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
youtubeArtist.albumsEndpoint?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "View all",
|
||||||
|
style = typography.xs.secondary,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onViewAllAlbumsClick
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = albums,
|
||||||
|
key = YouTube.Item.Album::key
|
||||||
|
) { album ->
|
||||||
|
AlternativeAlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = albumThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = { onAlbumClick(album.key) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeArtist.singles?.let { singles ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Singles",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
youtubeArtist.singlesEndpoint?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "View all",
|
||||||
|
style = typography.xs.secondary,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = onViewAllSinglesClick
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = singles,
|
||||||
|
key = YouTube.Item.Album::key
|
||||||
|
) { album ->
|
||||||
|
AlternativeAlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = albumThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = { onAlbumClick(album.key) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isError -> ErrorText()
|
||||||
|
isLoading -> ShimmerHost {
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
repeat(5) {
|
||||||
|
SmallSongItemShimmer(
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat(2) {
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
repeat(2) {
|
||||||
|
AlternativeAlbumItemPlaceholder(thumbnailSizeDp = albumThumbnailSizeDp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isError -> ErrorText()
|
||||||
|
isLoading -> ShimmerHost {
|
||||||
|
HeaderPlaceholder()
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
.background(colorPalette.shimmer)
|
||||||
|
)
|
||||||
|
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
repeat(5) {
|
||||||
|
SmallSongItemShimmer(
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat(2) {
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
repeat(2) {
|
||||||
|
AlternativeAlbumItemPlaceholder(thumbnailSizeDp = albumThumbnailSizeDp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeArtist?.shuffleEndpoint?.let { shuffleEndpoint ->
|
||||||
|
PrimaryButton(
|
||||||
|
iconId = R.drawable.shuffle,
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.playRadio(shuffleEndpoint)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.ErrorText() {
|
||||||
|
BasicText(
|
||||||
|
text = "An error has occurred",
|
||||||
|
style = LocalAppearance.current.typography.s.secondary.center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShimmerHost(content: @Composable ColumnScope.() -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.shimmer()
|
||||||
|
.graphicsLayer(alpha = 0.99f)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
listOf(Color.Black, Color.Transparent)
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +1,182 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.artist
|
package it.vfsfitvnm.vimusic.ui.screens.artist
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.geometry.center
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.models.Artist
|
import it.vfsfitvnm.vimusic.models.PartialArtist
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
import it.vfsfitvnm.vimusic.savers.ArtistSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeArtistPageSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResult
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
import it.vfsfitvnm.vimusic.ui.views.AlbumItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.AlbumItemShimmer
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SmallSongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.artistScreenTabIndexKey
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
import it.vfsfitvnm.vimusic.utils.produceSaveableLazyOneShotState
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun ArtistScreen(browseId: String) {
|
fun ArtistScreen(browseId: String) {
|
||||||
val saveableStateHolder = rememberSaveableStateHolder()
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
val (tabIndex, onTabIndexChanged) = rememberSaveable {
|
val (tabIndex, onTabIndexChanged) = rememberPreference(
|
||||||
mutableStateOf(0)
|
artistScreenTabIndexKey,
|
||||||
|
defaultValue = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
var isLoading by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isError by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val youtubeArtist by produceSaveableLazyOneShotState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(YouTubeArtistPageSaver)
|
||||||
|
) {
|
||||||
|
println("${System.currentTimeMillis()}, computing lazyEffect (youtubeArtistResult = ${value?.name})!")
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
YouTube.artist(browseId)?.onSuccess { youtubeArtist ->
|
||||||
|
value = youtubeArtist
|
||||||
|
|
||||||
|
query {
|
||||||
|
Database.upsert(
|
||||||
|
PartialArtist(
|
||||||
|
id = browseId,
|
||||||
|
name = youtubeArtist.name,
|
||||||
|
thumbnailUrl = youtubeArtist.thumbnail?.url,
|
||||||
|
info = youtubeArtist.description,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isError = false
|
||||||
|
isLoading = false
|
||||||
|
}?.onFailure {
|
||||||
|
println("error (1): $it")
|
||||||
|
isError = true
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val artist by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(ArtistSaver),
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.artist(browseId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.filter {
|
||||||
|
val hasToFetch = it?.timestamp == null
|
||||||
|
if (hasToFetch) {
|
||||||
|
youtubeArtist?.name
|
||||||
|
}
|
||||||
|
!hasToFetch
|
||||||
|
}
|
||||||
|
.collect { value = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
globalRoutes()
|
globalRoutes()
|
||||||
|
|
||||||
host {
|
host {
|
||||||
|
val bookmarkIconContent: @Composable () -> Unit = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(
|
||||||
|
if (artist?.bookmarkedAt == null) {
|
||||||
|
R.drawable.bookmark_outline
|
||||||
|
} else {
|
||||||
|
R.drawable.bookmark
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.accent),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
val bookmarkedAt = if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null
|
||||||
|
|
||||||
|
query {
|
||||||
|
artist?.copy(bookmarkedAt = bookmarkedAt)?.let(Database::update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareIconContent: @Composable () -> Unit = {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.share_social),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
val sendIntent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
"https://music.youtube.com/channel/$browseId"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
sendIntent,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topIconButtonId = R.drawable.chevron_back,
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
onTopIconButtonClick = pop,
|
onTopIconButtonClick = pop,
|
||||||
@@ -92,273 +187,151 @@ fun ArtistScreen(browseId: String) {
|
|||||||
Item(1, "Songs", R.drawable.musical_notes)
|
Item(1, "Songs", R.drawable.musical_notes)
|
||||||
Item(2, "Albums", R.drawable.disc)
|
Item(2, "Albums", R.drawable.disc)
|
||||||
Item(3, "Singles", R.drawable.disc)
|
Item(3, "Singles", R.drawable.disc)
|
||||||
|
Item(4, "Library", R.drawable.library)
|
||||||
}
|
}
|
||||||
) { currentTabIndex ->
|
) { currentTabIndex ->
|
||||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
ArtistOverview(browseId = browseId)
|
when (currentTabIndex) {
|
||||||
}
|
0 -> ArtistOverview(
|
||||||
}
|
artist = artist,
|
||||||
}
|
youtubeArtist = youtubeArtist,
|
||||||
}
|
isLoading = isLoading,
|
||||||
}
|
isError = isError,
|
||||||
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
@ExperimentalAnimationApi
|
shareIconContent = shareIconContent,
|
||||||
@Composable
|
onAlbumClick = { albumRoute(it) },
|
||||||
fun ArtistScreen2(browseId: String) {
|
onViewAllSongsClick = { onTabIndexChanged(1) },
|
||||||
val lazyListState = rememberLazyListState()
|
onViewAllAlbumsClick = { onTabIndexChanged(2) },
|
||||||
|
onViewAllSinglesClick = { onTabIndexChanged(3) },
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
val artistResult by remember(browseId) {
|
|
||||||
Database.artist(browseId).map { artist ->
|
|
||||||
artist
|
|
||||||
?.takeIf { artist.timestamp != null }
|
|
||||||
?.let(Result.Companion::success)
|
|
||||||
?: fetchArtist(browseId)
|
|
||||||
}.distinctUntilChanged()
|
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
|
||||||
|
|
||||||
val songThumbnailSizePx = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
val songs by remember(browseId) {
|
|
||||||
Database.artistSongs(browseId)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
artistResult?.getOrNull()?.let { artist ->
|
|
||||||
AsyncImage(
|
|
||||||
model = artist.thumbnailUrl?.thumbnail(Dimensions.thumbnails.artist.px),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.size(Dimensions.thumbnails.artist)
|
|
||||||
)
|
)
|
||||||
|
1 -> {
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
BasicText(
|
ArtistContent(
|
||||||
text = artist.name,
|
artist = artist,
|
||||||
style = typography.l.semiBold,
|
youtubeArtist = youtubeArtist,
|
||||||
modifier = Modifier
|
isLoading = isLoading,
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
isError = isError,
|
||||||
)
|
stateSaver = YouTubeSongListSaver,
|
||||||
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
Row(
|
shareIconContent = shareIconContent,
|
||||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
itemsProvider = { continuation ->
|
||||||
modifier = Modifier
|
youtubeArtist
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
?.songsEndpoint
|
||||||
) {
|
?.browseId
|
||||||
Image(
|
?.let { browseId ->
|
||||||
painter = painterResource(R.drawable.shuffle),
|
YouTube.items(browseId, continuation, YouTube.Item.Song::from)?.map { result ->
|
||||||
contentDescription = null,
|
result?.continuation to result?.items
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
binder?.playRadio(
|
|
||||||
NavigationEndpoint.Endpoint.Watch(
|
|
||||||
videoId = artist.shuffleVideoId,
|
|
||||||
playlistId = artist.shufflePlaylistId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
query {
|
|
||||||
runBlocking {
|
|
||||||
fetchArtist(browseId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
.padding(all = 8.dp)
|
itemContent = { song ->
|
||||||
.size(20.dp)
|
SmallSongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlay(song.asMediaItem)
|
||||||
|
binder?.setupRadio(song.info?.endpoint)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
itemShimmer = {
|
||||||
|
SmallSongItemShimmer(thumbnailSizeDp = thumbnailSizeDp)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
val thumbnailSizeDp = 108.dp
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
Image(
|
ArtistContent(
|
||||||
painter = painterResource(R.drawable.radio),
|
artist = artist,
|
||||||
contentDescription = null,
|
youtubeArtist = youtubeArtist,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
isLoading = isLoading,
|
||||||
modifier = Modifier
|
isError = isError,
|
||||||
.clickable {
|
stateSaver = YouTubeAlbumListSaver,
|
||||||
binder?.playRadio(
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
NavigationEndpoint.Endpoint.Watch(
|
shareIconContent = shareIconContent,
|
||||||
videoId = artist.radioVideoId
|
itemsProvider = {
|
||||||
?: artist.shuffleVideoId,
|
youtubeArtist
|
||||||
playlistId = artist.radioPlaylistId
|
?.albumsEndpoint
|
||||||
)
|
?.let { endpoint ->
|
||||||
)
|
YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result ->
|
||||||
|
result?.continuation to result?.items
|
||||||
query {
|
|
||||||
runBlocking {
|
|
||||||
fetchArtist(browseId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
.padding(all = 8.dp)
|
itemContent = { album ->
|
||||||
.size(20.dp)
|
AlbumItem(
|
||||||
)
|
album = album,
|
||||||
}
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
} ?: artistResult?.exceptionOrNull()?.let { throwable ->
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
// LoadingOrError(
|
modifier = Modifier
|
||||||
// errorMessage = throwable.javaClass.canonicalName,
|
.clickable(
|
||||||
// onRetry = {
|
indication = rememberRipple(bounded = true),
|
||||||
// query {
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
// runBlocking {
|
onClick = { albumRoute(album.info?.endpoint?.browseId) }
|
||||||
// Database.artist(browseId).first()?.let(Database::update)
|
)
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item("songs") {
|
|
||||||
if (songs.isEmpty()) return@item
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.zIndex(1f)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 32.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Local tracks",
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = songs.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
songs
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
itemShimmer = {
|
||||||
|
AlbumItemShimmer(thumbnailSizeDp = thumbnailSizeDp)
|
||||||
}
|
}
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = songs,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song },
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
song = song,
|
|
||||||
thumbnailSizePx = songThumbnailSizePx,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
songs.map(DetailedSong::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
InHistoryMediaItemMenu(song = song)
|
|
||||||
}
|
}
|
||||||
)
|
3 -> {
|
||||||
}
|
val thumbnailSizeDp = 108.dp
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
artistResult?.getOrNull()?.info?.let { description ->
|
ArtistContent(
|
||||||
item {
|
artist = artist,
|
||||||
Column(
|
youtubeArtist = youtubeArtist,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
isLoading = isLoading,
|
||||||
modifier = Modifier
|
isError = isError,
|
||||||
.background(colorPalette.background0)
|
stateSaver = YouTubeAlbumListSaver,
|
||||||
.fillMaxWidth()
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
.padding(horizontal = 8.dp)
|
shareIconContent = shareIconContent,
|
||||||
.padding(top = 32.dp)
|
itemsProvider = {
|
||||||
) {
|
youtubeArtist
|
||||||
BasicText(
|
?.singlesEndpoint
|
||||||
text = "Information",
|
?.let { endpoint ->
|
||||||
style = typography.m.semiBold,
|
YouTube.items2(browseId, endpoint.params, YouTube.Item.Album::from)?.map { result ->
|
||||||
modifier = Modifier
|
result?.continuation to result?.items
|
||||||
.padding(horizontal = 8.dp)
|
}
|
||||||
)
|
}
|
||||||
|
},
|
||||||
Row(
|
itemContent = { album ->
|
||||||
modifier = Modifier
|
AlbumItem(
|
||||||
.height(IntrinsicSize.Max)
|
album = album,
|
||||||
.padding(all = 8.dp)
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
.fillMaxWidth()
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
) {
|
modifier = Modifier
|
||||||
Canvas(
|
.clickable(
|
||||||
modifier = Modifier
|
indication = rememberRipple(bounded = true),
|
||||||
.fillMaxHeight()
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
.width(48.dp)
|
onClick = { albumRoute(album.info?.endpoint?.browseId) }
|
||||||
) {
|
)
|
||||||
drawLine(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
start = size.center.copy(y = 0f),
|
|
||||||
end = size.center.copy(y = size.height),
|
|
||||||
strokeWidth = 2.dp.toPx()
|
|
||||||
)
|
|
||||||
|
|
||||||
drawCircle(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
center = size.center.copy(y = size.height),
|
|
||||||
radius = 4.dp.toPx()
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
itemShimmer = {
|
||||||
|
AlbumItemShimmer(thumbnailSizeDp = thumbnailSizeDp)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
BasicText(
|
|
||||||
text = description,
|
|
||||||
style = typography.xxs.secondary.medium.copy(
|
|
||||||
lineHeight = 24.sp,
|
|
||||||
textAlign = TextAlign.Justify
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
4 -> ArtistLocalSongsList(
|
||||||
|
browseId = browseId,
|
||||||
|
artist = artist,
|
||||||
|
isLoading = isLoading,
|
||||||
|
isError = isError,
|
||||||
|
bookmarkIconContent = bookmarkIconContent,
|
||||||
|
shareIconContent = shareIconContent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun fetchArtist(browseId: String): Result<Artist>? {
|
|
||||||
return YouTube.artist(browseId)
|
|
||||||
?.map { youtubeArtist ->
|
|
||||||
Artist(
|
|
||||||
id = browseId,
|
|
||||||
name = youtubeArtist.name ?: "",
|
|
||||||
thumbnailUrl = youtubeArtist.thumbnail?.url,
|
|
||||||
info = youtubeArtist.description,
|
|
||||||
shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,
|
|
||||||
shufflePlaylistId = youtubeArtist.shuffleEndpoint?.playlistId,
|
|
||||||
radioVideoId = youtubeArtist.radioEndpoint?.videoId,
|
|
||||||
radioPlaylistId = youtubeArtist.radioEndpoint?.playlistId,
|
|
||||||
timestamp = System.currentTimeMillis()
|
|
||||||
).also(Database::upsert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ fun HomeArtistList(
|
|||||||
)
|
)
|
||||||
|
|
||||||
BasicText(
|
BasicText(
|
||||||
text = artist.name,
|
text = artist.name ?: "",
|
||||||
style = typography.xxs.semiBold.center,
|
style = typography.xxs.semiBold.center,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
|
|||||||
@@ -46,14 +46,13 @@ inline fun <T : YouTube.Item> SearchResult(
|
|||||||
) {
|
) {
|
||||||
val (_, typography) = LocalAppearance.current
|
val (_, typography) = LocalAppearance.current
|
||||||
|
|
||||||
var items by rememberSaveable(query, filter, stateSaver = stateSaver) {
|
var items by rememberSaveable(stateSaver = stateSaver) {
|
||||||
mutableStateOf(listOf())
|
mutableStateOf(listOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState(
|
val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
stateSaver = StringResultSaver,
|
stateSaver = StringResultSaver
|
||||||
query, filter
|
|
||||||
) {
|
) {
|
||||||
val token = value?.getOrNull()
|
val token = value?.getOrNull()
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
@@ -380,6 +381,75 @@ fun AlbumItemShimmer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlternativeAlbumItem(
|
||||||
|
album: YouTube.Item.Album,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val (_, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||||
|
.width(thumbnailSizeDp)
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = album.thumbnail?.size(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
BasicText(
|
||||||
|
text = album.info?.name ?: "",
|
||||||
|
style = typography.xs.semiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = album.year ?: "",
|
||||||
|
style = typography.xxs.semiBold.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlternativeAlbumItemPlaceholder(
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||||
|
.width(thumbnailSizeDp)
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ArtistItem(
|
fun ArtistItem(
|
||||||
artist: YouTube.Item.Artist,
|
artist: YouTube.Item.Artist,
|
||||||
|
|||||||
66
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyEffect.kt
Normal file
66
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/LazyEffect.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
@file:OptIn(InternalComposeApi::class)
|
||||||
|
|
||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.InternalComposeApi
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.runtime.ProduceStateScope
|
||||||
|
import androidx.compose.runtime.RememberObserver
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.currentComposer
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun lazyEffect(
|
||||||
|
key1: Any?,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): () -> Unit {
|
||||||
|
val applyContext = currentComposer.applyCoroutineContext
|
||||||
|
|
||||||
|
val lazyEffect = remember(key1) {
|
||||||
|
LazyEffectImpl(applyContext, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lazyEffect::calculate
|
||||||
|
}
|
||||||
|
|
||||||
|
class LazyEffectImpl(
|
||||||
|
parentCoroutineContext: CoroutineContext,
|
||||||
|
private val task: suspend CoroutineScope.() -> Unit
|
||||||
|
) : RememberObserver {
|
||||||
|
private val scope = CoroutineScope(parentCoroutineContext)
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
fun calculate() {
|
||||||
|
if (job == null) {
|
||||||
|
job = scope.launch(block = task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemembered() = Unit
|
||||||
|
|
||||||
|
override fun onForgotten() {
|
||||||
|
job?.cancel()
|
||||||
|
job = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAbandoned() {
|
||||||
|
job?.cancel()
|
||||||
|
job = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ const val isShowingSynchronizedLyricsKey = "isShowingSynchronizedLyrics"
|
|||||||
const val isShowingThumbnailInLockscreenKey = "isShowingThumbnailInLockscreen"
|
const val isShowingThumbnailInLockscreenKey = "isShowingThumbnailInLockscreen"
|
||||||
const val homeScreenTabIndexKey = "homeScreenTabIndex"
|
const val homeScreenTabIndexKey = "homeScreenTabIndex"
|
||||||
const val searchResultScreenTabIndexKey = "searchResultScreenTabIndex"
|
const val searchResultScreenTabIndexKey = "searchResultScreenTabIndex"
|
||||||
|
const val artistScreenTabIndexKey = "artistScreenTabIndex"
|
||||||
|
|
||||||
inline fun <reified T : Enum<T>> SharedPreferences.getEnum(
|
inline fun <reified T : Enum<T>> SharedPreferences.getEnum(
|
||||||
key: String,
|
key: String,
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.ProduceStateScope
|
import androidx.compose.runtime.ProduceStateScope
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.Saver
|
import androidx.compose.runtime.saveable.Saver
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.experimental.ExperimentalTypeInference
|
import kotlin.experimental.ExperimentalTypeInference
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -123,19 +126,17 @@ fun <T> produceSaveableState(
|
|||||||
fun <T> produceSaveableRelaunchableOneShotState(
|
fun <T> produceSaveableRelaunchableOneShotState(
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
stateSaver: Saver<T, out Any>,
|
stateSaver: Saver<T, out Any>,
|
||||||
key1: Any?,
|
|
||||||
key2: Any?,
|
|
||||||
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
): Pair<State<T>, () -> Unit> {
|
): Pair<State<T>, () -> Unit> {
|
||||||
val result = rememberSaveable(stateSaver = stateSaver) {
|
val result = rememberSaveable(stateSaver = stateSaver) {
|
||||||
mutableStateOf(initialValue)
|
mutableStateOf(initialValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
var produced by rememberSaveable(key1, key2) {
|
var produced by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val relaunchableEffect = relaunchableEffect(key1, key2) {
|
val relaunchableEffect = relaunchableEffect(Unit) {
|
||||||
if (!produced) {
|
if (!produced) {
|
||||||
ProduceSaveableStateScope(result, coroutineContext).producer()
|
ProduceSaveableStateScope(result, coroutineContext).producer()
|
||||||
produced = true
|
produced = true
|
||||||
@@ -148,6 +149,70 @@ fun <T> produceSaveableRelaunchableOneShotState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableRelaunchableOneShotState(
|
||||||
|
initialValue: T,
|
||||||
|
stateSaver: Saver<T, out Any>,
|
||||||
|
key1: Any?,
|
||||||
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
|
): Pair<State<T>, () -> Unit> {
|
||||||
|
val result = rememberSaveable(stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var produced by rememberSaveable(key1) {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val relaunchableEffect = relaunchableEffect(key1) {
|
||||||
|
if (!produced) {
|
||||||
|
ProduceSaveableStateScope(result, coroutineContext).producer()
|
||||||
|
produced = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result to {
|
||||||
|
produced = false
|
||||||
|
relaunchableEffect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableLazyOneShotState(
|
||||||
|
initialValue: T,
|
||||||
|
stateSaver: Saver<T, out Any>,
|
||||||
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
|
): State<T> {
|
||||||
|
val state = rememberSaveable(stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var produced by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val lazyEffect = lazyEffect(Unit) {
|
||||||
|
if (!produced) {
|
||||||
|
ProduceSaveableStateScope(state, coroutineContext).producer()
|
||||||
|
produced = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val delegate = remember {
|
||||||
|
object : State<T> {
|
||||||
|
override val value: T
|
||||||
|
get() {
|
||||||
|
if (!produced) {
|
||||||
|
lazyEffect()
|
||||||
|
}
|
||||||
|
return state.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return delegate
|
||||||
|
}
|
||||||
|
|
||||||
private class ProduceSaveableStateScope<T>(
|
private class ProduceSaveableStateScope<T>(
|
||||||
state: MutableState<T>,
|
state: MutableState<T>,
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ dependencies {
|
|||||||
implementation(libs.ktor.serialization.json)
|
implementation(libs.ktor.serialization.json)
|
||||||
|
|
||||||
testImplementation(testLibs.junit)
|
testImplementation(testLibs.junit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ object YouTube {
|
|||||||
data class BrowseBody(
|
data class BrowseBody(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val browseId: String,
|
val browseId: String,
|
||||||
|
val params: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -560,7 +561,7 @@ object YouTube {
|
|||||||
} else {
|
} else {
|
||||||
response.body<ContinuationResponse>()
|
response.body<ContinuationResponse>()
|
||||||
.continuationContents
|
.continuationContents
|
||||||
.musicShelfContinuation
|
?.musicShelfContinuation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SearchResult(
|
SearchResult(
|
||||||
@@ -580,7 +581,7 @@ object YouTube {
|
|||||||
continuation = musicShelfRenderer
|
continuation = musicShelfRenderer
|
||||||
?.continuations
|
?.continuations
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.nextRadioContinuationData
|
?.nextContinuationData
|
||||||
?.continuation
|
?.continuation
|
||||||
)
|
)
|
||||||
}.recoverIfCancelled()
|
}.recoverIfCancelled()
|
||||||
@@ -785,7 +786,7 @@ object YouTube {
|
|||||||
?.playlistPanelRenderer
|
?.playlistPanelRenderer
|
||||||
?.continuations
|
?.continuations
|
||||||
?.getOrNull(0)
|
?.getOrNull(0)
|
||||||
?.nextRadioContinuationData
|
?.nextContinuationData
|
||||||
?.continuation,
|
?.continuation,
|
||||||
items = (tabs
|
items = (tabs
|
||||||
.getOrNull(0)
|
.getOrNull(0)
|
||||||
@@ -868,9 +869,9 @@ object YouTube {
|
|||||||
} else {
|
} else {
|
||||||
browse(lyricsBrowseId)?.map { body ->
|
browse(lyricsBrowseId)?.map { body ->
|
||||||
body.contents
|
body.contents
|
||||||
.sectionListRenderer
|
?.sectionListRenderer
|
||||||
?.contents
|
?.contents
|
||||||
?.first()
|
?.firstOrNull()
|
||||||
?.musicDescriptionShelfRenderer
|
?.musicDescriptionShelfRenderer
|
||||||
?.description
|
?.description
|
||||||
?.text
|
?.text
|
||||||
@@ -895,6 +896,105 @@ object YouTube {
|
|||||||
}.recoverIfCancelled()
|
}.recoverIfCancelled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ItemsResult<T : Item>(
|
||||||
|
val items: List<T>?,
|
||||||
|
val continuation: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun <T : Item> items(
|
||||||
|
browseId: String,
|
||||||
|
continuation: String?,
|
||||||
|
block: (MusicResponsiveListItemRenderer) -> T?
|
||||||
|
): Result<ItemsResult<T>?>? {
|
||||||
|
return runCatching {
|
||||||
|
val response = client.post("/youtubei/v1/browse") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
BrowseBody(
|
||||||
|
browseId = browseId,
|
||||||
|
context = Context.DefaultWeb
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parameter("key", Key)
|
||||||
|
parameter("prettyPrint", false)
|
||||||
|
parameter("continuation", continuation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (continuation == null) {
|
||||||
|
response
|
||||||
|
.body<BrowseResponse>()
|
||||||
|
.contents
|
||||||
|
?.singleColumnBrowseResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.contents
|
||||||
|
?.firstOrNull()
|
||||||
|
?.musicShelfRenderer
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
.body<ContinuationResponse>()
|
||||||
|
.continuationContents
|
||||||
|
?.musicShelfContinuation
|
||||||
|
}?.let { musicShelfRenderer ->
|
||||||
|
ItemsResult(
|
||||||
|
items = musicShelfRenderer
|
||||||
|
.contents
|
||||||
|
.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
|
.mapNotNull(block),
|
||||||
|
continuation = musicShelfRenderer
|
||||||
|
.continuations
|
||||||
|
?.firstOrNull()
|
||||||
|
?.nextContinuationData
|
||||||
|
?.continuation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.recoverIfCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T : Item> items2(
|
||||||
|
browseId: String,
|
||||||
|
params: String?,
|
||||||
|
block: (MusicTwoRowItemRenderer) -> T?
|
||||||
|
): Result<ItemsResult<T>?>? {
|
||||||
|
return runCatching {
|
||||||
|
client.post("/youtubei/v1/browse") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
BrowseBody(
|
||||||
|
browseId = browseId,
|
||||||
|
context = Context.DefaultWeb,
|
||||||
|
params = params
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parameter("key", Key)
|
||||||
|
parameter("prettyPrint", false)
|
||||||
|
}
|
||||||
|
.body<BrowseResponse>()
|
||||||
|
.contents
|
||||||
|
?.singleColumnBrowseResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.contents
|
||||||
|
?.firstOrNull()
|
||||||
|
?.gridRenderer
|
||||||
|
?.let { gridRenderer ->
|
||||||
|
ItemsResult(
|
||||||
|
items = gridRenderer
|
||||||
|
.items
|
||||||
|
?.mapNotNull(SectionListRenderer.Content.GridRenderer.Item::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(block),
|
||||||
|
continuation = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.recoverIfCancelled()
|
||||||
|
}
|
||||||
|
|
||||||
data class PlaylistOrAlbum(
|
data class PlaylistOrAlbum(
|
||||||
val title: String?,
|
val title: String?,
|
||||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
@@ -918,17 +1018,17 @@ object YouTube {
|
|||||||
songs = songs?.plus(
|
songs = songs?.plus(
|
||||||
continuationResponse
|
continuationResponse
|
||||||
.continuationContents
|
.continuationContents
|
||||||
.musicShelfContinuation
|
?.musicShelfContinuation
|
||||||
?.contents
|
?.contents
|
||||||
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
?.mapNotNull(Item.Song.Companion::from) ?: emptyList()
|
?.mapNotNull(Item.Song.Companion::from) ?: emptyList()
|
||||||
),
|
),
|
||||||
continuation = continuationResponse
|
continuation = continuationResponse
|
||||||
.continuationContents
|
.continuationContents
|
||||||
.musicShelfContinuation
|
?.musicShelfContinuation
|
||||||
?.continuations
|
?.continuations
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.nextRadioContinuationData
|
?.nextContinuationData
|
||||||
?.continuation
|
?.continuation
|
||||||
).next()
|
).next()
|
||||||
}
|
}
|
||||||
@@ -1003,7 +1103,7 @@ object YouTube {
|
|||||||
?.text,
|
?.text,
|
||||||
songs = body
|
songs = body
|
||||||
.contents
|
.contents
|
||||||
.singleColumnBrowseResultsRenderer
|
?.singleColumnBrowseResultsRenderer
|
||||||
?.tabs
|
?.tabs
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.tabRenderer
|
?.tabRenderer
|
||||||
@@ -1021,7 +1121,7 @@ object YouTube {
|
|||||||
?.urlCanonical,
|
?.urlCanonical,
|
||||||
continuation = body
|
continuation = body
|
||||||
.contents
|
.contents
|
||||||
.singleColumnBrowseResultsRenderer
|
?.singleColumnBrowseResultsRenderer
|
||||||
?.tabs
|
?.tabs
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.tabRenderer
|
?.tabRenderer
|
||||||
@@ -1032,7 +1132,7 @@ object YouTube {
|
|||||||
?.musicShelfRenderer
|
?.musicShelfRenderer
|
||||||
?.continuations
|
?.continuations
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.nextRadioContinuationData
|
?.nextContinuationData
|
||||||
?.continuation
|
?.continuation
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1043,24 +1143,61 @@ object YouTube {
|
|||||||
val description: String?,
|
val description: String?,
|
||||||
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
||||||
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
||||||
val radioEndpoint: NavigationEndpoint.Endpoint.Watch?
|
val radioEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
||||||
|
val songs: List<Item.Song>?,
|
||||||
|
val songsEndpoint: NavigationEndpoint.Endpoint.Browse?,
|
||||||
|
val albums: List<Item.Album>?,
|
||||||
|
val albumsEndpoint: NavigationEndpoint.Endpoint.Browse?,
|
||||||
|
val singles: List<Item.Album>?,
|
||||||
|
val singlesEndpoint: NavigationEndpoint.Endpoint.Browse?,
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun artist(browseId: String): Result<Artist>? {
|
suspend fun artist(browseId: String): Result<Artist>? {
|
||||||
return browse(browseId)?.map { body ->
|
return browse(browseId)?.map { response ->
|
||||||
|
fun findSectionByTitle(text: String): SectionListRenderer.Content? {
|
||||||
|
return response
|
||||||
|
.contents
|
||||||
|
?.singleColumnBrowseResultsRenderer
|
||||||
|
?.tabs
|
||||||
|
?.get(0)
|
||||||
|
?.tabRenderer
|
||||||
|
?.content
|
||||||
|
?.sectionListRenderer
|
||||||
|
?.contents
|
||||||
|
?.find { content ->
|
||||||
|
val title = content
|
||||||
|
.musicCarouselShelfRenderer
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.title
|
||||||
|
?: content
|
||||||
|
.musicShelfRenderer
|
||||||
|
?.title
|
||||||
|
|
||||||
|
title
|
||||||
|
?.runs
|
||||||
|
?.firstOrNull()
|
||||||
|
?.text == text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val songsSection = findSectionByTitle("Songs")?.musicShelfRenderer
|
||||||
|
val albumsSection = findSectionByTitle("Albums")?.musicCarouselShelfRenderer
|
||||||
|
val singlesSection = findSectionByTitle("Singles")?.musicCarouselShelfRenderer
|
||||||
|
|
||||||
Artist(
|
Artist(
|
||||||
name = body
|
name = response
|
||||||
.header
|
.header
|
||||||
?.musicImmersiveHeaderRenderer
|
?.musicImmersiveHeaderRenderer
|
||||||
?.title
|
?.title
|
||||||
?.text,
|
?.text,
|
||||||
description = body
|
description = response
|
||||||
.header
|
.header
|
||||||
?.musicImmersiveHeaderRenderer
|
?.musicImmersiveHeaderRenderer
|
||||||
?.description
|
?.description
|
||||||
?.text
|
?.text
|
||||||
?.substringBeforeLast("\n\nFrom Wikipedia"),
|
?.substringBeforeLast("\n\nFrom Wikipedia"),
|
||||||
thumbnail = body
|
thumbnail = response
|
||||||
.header
|
.header
|
||||||
?.musicImmersiveHeaderRenderer
|
?.musicImmersiveHeaderRenderer
|
||||||
?.thumbnail
|
?.thumbnail
|
||||||
@@ -1068,20 +1205,49 @@ object YouTube {
|
|||||||
?.thumbnail
|
?.thumbnail
|
||||||
?.thumbnails
|
?.thumbnails
|
||||||
?.getOrNull(0),
|
?.getOrNull(0),
|
||||||
shuffleEndpoint = body
|
shuffleEndpoint = response
|
||||||
.header
|
.header
|
||||||
?.musicImmersiveHeaderRenderer
|
?.musicImmersiveHeaderRenderer
|
||||||
?.playButton
|
?.playButton
|
||||||
?.buttonRenderer
|
?.buttonRenderer
|
||||||
?.navigationEndpoint
|
?.navigationEndpoint
|
||||||
?.watchEndpoint,
|
?.watchEndpoint,
|
||||||
radioEndpoint = body
|
radioEndpoint = response
|
||||||
.header
|
.header
|
||||||
?.musicImmersiveHeaderRenderer
|
?.musicImmersiveHeaderRenderer
|
||||||
?.startRadioButton
|
?.startRadioButton
|
||||||
?.buttonRenderer
|
?.buttonRenderer
|
||||||
?.navigationEndpoint
|
?.navigationEndpoint
|
||||||
?.watchEndpoint
|
?.watchEndpoint,
|
||||||
|
songs = songsSection
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
|
?.mapNotNull(Item.Song::from),
|
||||||
|
songsEndpoint = songsSection
|
||||||
|
?.bottomEndpoint
|
||||||
|
?.browseEndpoint,
|
||||||
|
albums = albumsSection
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Item.Album::from),
|
||||||
|
albumsEndpoint = albumsSection
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.moreContentButton
|
||||||
|
?.buttonRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.browseEndpoint,
|
||||||
|
singles = singlesSection
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Item.Album::from),
|
||||||
|
singlesEndpoint = singlesSection
|
||||||
|
?.header
|
||||||
|
?.musicCarouselShelfBasicHeaderRenderer
|
||||||
|
?.moreContentButton
|
||||||
|
?.buttonRenderer
|
||||||
|
?.navigationEndpoint
|
||||||
|
?.browseEndpoint,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1132,7 +1298,7 @@ object YouTube {
|
|||||||
browse(browseId)?.getOrThrow()?.let { browseResponse ->
|
browse(browseId)?.getOrThrow()?.let { browseResponse ->
|
||||||
browseResponse
|
browseResponse
|
||||||
.contents
|
.contents
|
||||||
.sectionListRenderer
|
?.sectionListRenderer
|
||||||
?.contents
|
?.contents
|
||||||
?.mapNotNull(SectionListRenderer.Content::musicCarouselShelfRenderer)
|
?.mapNotNull(SectionListRenderer.Content::musicCarouselShelfRenderer)
|
||||||
?.map(MusicCarouselShelfRenderer::contents)
|
?.map(MusicCarouselShelfRenderer::contents)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BrowseResponse(
|
data class BrowseResponse(
|
||||||
val contents: Contents,
|
val contents: Contents?,
|
||||||
val header: Header?,
|
val header: Header?,
|
||||||
val microformat: Microformat?
|
val microformat: Microformat?
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import kotlinx.serialization.json.JsonNames
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Continuation(
|
data class Continuation(
|
||||||
@JsonNames("nextContinuationData", "nextRadioContinuationData")
|
@JsonNames("nextContinuationData", "nextRadioContinuationData")
|
||||||
val nextRadioContinuationData: Data
|
val nextContinuationData: Data
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Data(
|
data class Data(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import kotlinx.serialization.json.JsonNames
|
|||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ContinuationResponse(
|
data class ContinuationResponse(
|
||||||
val continuationContents: ContinuationContents,
|
val continuationContents: ContinuationContents?,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ContinuationContents(
|
data class ContinuationContents(
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package it.vfsfitvnm.youtubemusic.models
|
package it.vfsfitvnm.youtubemusic.models
|
||||||
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MusicShelfRenderer(
|
data class MusicShelfRenderer(
|
||||||
val bottomEndpoint: NavigationEndpoint?,
|
val bottomEndpoint: NavigationEndpoint?,
|
||||||
|
|||||||
@@ -44,11 +44,12 @@ data class SectionListRenderer(
|
|||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GridRenderer(
|
data class GridRenderer(
|
||||||
val items: List<Item>,
|
val items: List<Item>?,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Item(
|
data class Item(
|
||||||
val musicNavigationButtonRenderer: MusicNavigationButtonRenderer
|
val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?,
|
||||||
|
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user