Redesign AlbumScreen (#172)
This commit is contained in:
602
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json
Normal file
602
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 18,
|
||||||
|
"identityHash": "dec162db7ec49f4324481d54c49a793d",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Song",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistsText",
|
||||||
|
"columnName": "artistsText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "durationText",
|
||||||
|
"columnName": "durationText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lyrics",
|
||||||
|
"columnName": "lyrics",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "synchronizedLyrics",
|
||||||
|
"columnName": "synchronizedLyrics",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "likedAt",
|
||||||
|
"columnName": "likedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "totalPlayTimeMs",
|
||||||
|
"columnName": "totalPlayTimeMs",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SongPlaylistMap",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistId",
|
||||||
|
"columnName": "playlistId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId",
|
||||||
|
"playlistId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SongPlaylistMap_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_SongPlaylistMap_playlistId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlistId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "Playlist",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"playlistId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Playlist",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "browseId",
|
||||||
|
"columnName": "browseId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Artist",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "info",
|
||||||
|
"columnName": "info",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shuffleVideoId",
|
||||||
|
"columnName": "shuffleVideoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shufflePlaylistId",
|
||||||
|
"columnName": "shufflePlaylistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "radioVideoId",
|
||||||
|
"columnName": "radioVideoId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "radioPlaylistId",
|
||||||
|
"columnName": "radioPlaylistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SongArtistMap",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId",
|
||||||
|
"artistId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SongArtistMap_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_SongArtistMap_artistId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"artistId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "Artist",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"artistId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Album",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, 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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SongAlbumMap",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumId",
|
||||||
|
"columnName": "albumId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId",
|
||||||
|
"albumId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SongAlbumMap_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_SongAlbumMap_albumId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"albumId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "Album",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"albumId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SearchQuery",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "query",
|
||||||
|
"columnName": "query",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SearchQuery_query",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"query"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "QueuedMediaItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaItem",
|
||||||
|
"columnName": "mediaItem",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Format",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itag",
|
||||||
|
"columnName": "itag",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mimeType",
|
||||||
|
"columnName": "mimeType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bitrate",
|
||||||
|
"columnName": "bitrate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentLength",
|
||||||
|
"columnName": "contentLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastModified",
|
||||||
|
"columnName": "lastModified",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "loudnessDb",
|
||||||
|
"columnName": "loudnessDb",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"viewName": "SortedSongPlaylistMap",
|
||||||
|
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"viewName": "SortedSongAlbumMap",
|
||||||
|
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dec162db7ec49f4324481d54c49a793d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
|||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
import it.vfsfitvnm.vimusic.models.Album
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
|
import it.vfsfitvnm.vimusic.models.AlbumWithSongs
|
||||||
import it.vfsfitvnm.vimusic.models.Artist
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
|
import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
|
||||||
@@ -45,6 +46,7 @@ import it.vfsfitvnm.vimusic.models.Song
|
|||||||
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
||||||
import it.vfsfitvnm.vimusic.models.SongArtistMap
|
import it.vfsfitvnm.vimusic.models.SongArtistMap
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
||||||
|
import it.vfsfitvnm.vimusic.models.SortedSongAlbumMap
|
||||||
import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap
|
import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -144,8 +146,9 @@ 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?>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
@Query("SELECT * FROM Album WHERE id = :id")
|
@Query("SELECT * FROM Album WHERE id = :id")
|
||||||
fun album(id: String): Flow<Album?>
|
fun albumWithSongs(id: String): Flow<AlbumWithSongs?>
|
||||||
|
|
||||||
@Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id")
|
@Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id")
|
||||||
fun incrementTotalPlayTimeMs(id: String, addition: Long)
|
fun incrementTotalPlayTimeMs(id: String, addition: Long)
|
||||||
@@ -190,11 +193,6 @@ interface Database {
|
|||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
fun artistSongs(artistId: String): Flow<List<DetailedSong>>
|
fun artistSongs(artistId: String): Flow<List<DetailedSong>>
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
|
|
||||||
@RewriteQueriesToDropUnusedColumns
|
|
||||||
fun albumSongs(albumId: String): Flow<List<DetailedSong>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM Format WHERE songId = :songId")
|
@Query("SELECT * FROM Format WHERE songId = :songId")
|
||||||
fun format(songId: String): Flow<Format>
|
fun format(songId: String): Flow<Format>
|
||||||
|
|
||||||
@@ -361,9 +359,10 @@ interface Database {
|
|||||||
Format::class,
|
Format::class,
|
||||||
],
|
],
|
||||||
views = [
|
views = [
|
||||||
SortedSongPlaylistMap::class
|
SortedSongPlaylistMap::class,
|
||||||
|
SortedSongAlbumMap::class
|
||||||
],
|
],
|
||||||
version = 17,
|
version = 18,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -379,6 +378,7 @@ interface Database {
|
|||||||
AutoMigration(from = 13, to = 14),
|
AutoMigration(from = 13, to = 14),
|
||||||
AutoMigration(from = 15, to = 16),
|
AutoMigration(from = 15, to = 16),
|
||||||
AutoMigration(from = 16, to = 17),
|
AutoMigration(from = 16, to = 17),
|
||||||
|
AutoMigration(from = 17, to = 18),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.models
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Junction
|
||||||
|
import androidx.room.Relation
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class AlbumWithSongs(
|
||||||
|
@Embedded val album: Album,
|
||||||
|
@Relation(
|
||||||
|
entity = Song::class,
|
||||||
|
parentColumn = "id",
|
||||||
|
entityColumn = "id",
|
||||||
|
associateBy = Junction(
|
||||||
|
value = SortedSongAlbumMap::class,
|
||||||
|
parentColumn = "albumId",
|
||||||
|
entityColumn = "songId"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val songs: List<DetailedSong>
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.models
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.DatabaseView
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@DatabaseView("SELECT * FROM SongAlbumMap ORDER BY position")
|
||||||
|
data class SortedSongAlbumMap(
|
||||||
|
@ColumnInfo(index = true) val songId: String,
|
||||||
|
@ColumnInfo(index = true) val albumId: String,
|
||||||
|
val position: Int
|
||||||
|
)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.components.themed
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -11,12 +13,15 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
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.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Header(
|
fun Header(
|
||||||
@@ -69,3 +74,32 @@ fun Header(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HeaderPlaceholder(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.height(128.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.shimmer)
|
||||||
|
.fillMaxWidth(remember { 0.25f + Random.nextFloat() * 0.5f })
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "",
|
||||||
|
style = typography.xxl.medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,16 +112,11 @@ fun Scaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ModifierParameter")
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleScaffold(
|
fun SimpleScaffold(
|
||||||
topIconButtonId: Int,
|
topIconButtonId: Int,
|
||||||
onTopIconButtonClick: () -> Unit,
|
onTopIconButtonClick: () -> Unit,
|
||||||
title: String = "",
|
|
||||||
primaryIconButtonId: Int? = null,
|
|
||||||
primaryIconButtonEnabled: Boolean = true,
|
|
||||||
onPrimaryIconButtonClick: () -> Unit = {},
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -132,13 +127,9 @@ fun SimpleScaffold(
|
|||||||
.background(colorPalette.background0)
|
.background(colorPalette.background0)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
VerticalTitleBar(
|
VerticalBar(
|
||||||
topIconButtonId = topIconButtonId,
|
topIconButtonId = topIconButtonId,
|
||||||
onTopIconButtonClick = onTopIconButtonClick,
|
onTopIconButtonClick = onTopIconButtonClick,
|
||||||
title = title,
|
|
||||||
primaryIconButtonId = primaryIconButtonId,
|
|
||||||
primaryIconButtonEnabled = primaryIconButtonEnabled,
|
|
||||||
onPrimaryIconButtonClick = onPrimaryIconButtonClick,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(LocalPlayerAwarePaddingValues.current)
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TabColumn
|
import it.vfsfitvnm.vimusic.ui.components.TabColumn
|
||||||
import it.vfsfitvnm.vimusic.ui.components.vertical
|
import it.vfsfitvnm.vimusic.ui.components.vertical
|
||||||
|
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.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
|
||||||
@@ -35,8 +36,6 @@ fun VerticalBar(
|
|||||||
tabIndex: Int,
|
tabIndex: Int,
|
||||||
onTabChanged: (Int) -> Unit,
|
onTabChanged: (Int) -> Unit,
|
||||||
tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit,
|
tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||||
// primaryIconButtonId: Int? = null,
|
|
||||||
// onPrimaryIconButtonClick: () -> Unit = {},
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
@@ -44,6 +43,7 @@ fun VerticalBar(
|
|||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
// .width(Dimensions.verticalBarWidth)
|
||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
// Box(
|
// Box(
|
||||||
@@ -68,7 +68,7 @@ fun VerticalBar(
|
|||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(64.dp)
|
.width(Dimensions.verticalBarWidth)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,77 +110,28 @@ fun VerticalBar(
|
|||||||
|
|
||||||
@SuppressLint("ModifierParameter")
|
@SuppressLint("ModifierParameter")
|
||||||
@Composable
|
@Composable
|
||||||
fun VerticalTitleBar(
|
fun VerticalBar(
|
||||||
topIconButtonId: Int,
|
topIconButtonId: Int,
|
||||||
onTopIconButtonClick: () -> Unit,
|
onTopIconButtonClick: () -> Unit,
|
||||||
title: String,
|
|
||||||
primaryIconButtonId: Int? = null,
|
|
||||||
primaryIconButtonEnabled: Boolean = true,
|
|
||||||
onPrimaryIconButtonClick: () -> Unit = {},
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.width(Dimensions.verticalBarWidth)
|
||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
Image(
|
||||||
|
painter = painterResource(topIconButtonId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.clickable(onClick = onTopIconButtonClick)
|
.clickable(onClick = onTopIconButtonClick)
|
||||||
.background(color = colorPalette.background1)
|
.padding(all = 12.dp)
|
||||||
.size(48.dp)
|
.size(22.dp)
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(topIconButtonId),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(22.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(78.dp)
|
|
||||||
.height(32.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = title,
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.vertical()
|
|
||||||
.rotate(-90f)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
primaryIconButtonId?.let {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(x = 8.dp)
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.clickable(enabled = primaryIconButtonEnabled, onClick = onPrimaryIconButtonClick)
|
|
||||||
.background(colorPalette.background1)
|
|
||||||
.size(62.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(primaryIconButtonId),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,421 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
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.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.models.Album
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.bold
|
|
||||||
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.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun AlbumScreen(browseId: String) {
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
val albumResult by remember(browseId) {
|
|
||||||
Database.album(browseId).map { album ->
|
|
||||||
album
|
|
||||||
?.takeIf { album.timestamp != null }
|
|
||||||
?.let(Result.Companion::success)
|
|
||||||
?: YouTube.album(browseId)?.map { youtubeAlbum ->
|
|
||||||
Database.upsert(
|
|
||||||
Album(
|
|
||||||
id = browseId,
|
|
||||||
title = youtubeAlbum.title,
|
|
||||||
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
|
||||||
year = youtubeAlbum.year,
|
|
||||||
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
|
|
||||||
shareUrl = youtubeAlbum.url,
|
|
||||||
timestamp = System.currentTimeMillis()
|
|
||||||
),
|
|
||||||
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
|
|
||||||
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
SongAlbumMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
albumId = browseId,
|
|
||||||
position = position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.distinctUntilChanged()
|
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
|
||||||
|
|
||||||
val songs by remember(browseId) {
|
|
||||||
Database.albumSongs(browseId)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
albumResult?.getOrNull()?.let { album ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = album.thumbnailUrl?.thumbnail(Dimensions.thumbnails.album.px),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.album)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = album.title ?: "Unknown",
|
|
||||||
style = typography.m.semiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = album.authorsText ?: "",
|
|
||||||
style = typography.xs.secondary.semiBold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
|
|
||||||
album.year?.let { year ->
|
|
||||||
BasicText(
|
|
||||||
text = year,
|
|
||||||
style = typography.xs.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: albumResult?.exceptionOrNull()?.let { throwable ->
|
|
||||||
LoadingOrError(errorMessage = throwable.javaClass.canonicalName)
|
|
||||||
} ?: LoadingOrError()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
.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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ellipsis_horizontal),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
menuState.display {
|
|
||||||
Menu {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.enqueue,
|
|
||||||
text = "Enqueue",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
binder?.player?.enqueue(
|
|
||||||
songs.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.playlist,
|
|
||||||
text = "Import as playlist",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
albumResult
|
|
||||||
?.getOrNull()
|
|
||||||
?.let { album ->
|
|
||||||
query {
|
|
||||||
val playlistId =
|
|
||||||
Database.insert(
|
|
||||||
Playlist(
|
|
||||||
name = album.title
|
|
||||||
?: "Unknown"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
songs.forEachIndexed { index, song ->
|
|
||||||
Database.insert(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = song.id,
|
|
||||||
playlistId = playlistId,
|
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.share_social,
|
|
||||||
text = "Share",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
albumResult?.getOrNull()?.shareUrl?.let { url ->
|
|
||||||
val sendIntent = Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
type = "text/plain"
|
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
sendIntent,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.download,
|
|
||||||
text = "Refetch",
|
|
||||||
secondaryText = albumResult?.getOrNull()?.timestamp?.let { timestamp ->
|
|
||||||
"Last updated on ${
|
|
||||||
DateFormat
|
|
||||||
.getDateTimeInstance()
|
|
||||||
.format(Date(timestamp))
|
|
||||||
}"
|
|
||||||
},
|
|
||||||
isEnabled = albumResult?.getOrNull() != null,
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
query {
|
|
||||||
albumResult
|
|
||||||
?.getOrNull()
|
|
||||||
?.let(Database::delete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = songs,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song }
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
title = song.title,
|
|
||||||
authors = song.artistsText ?: albumResult?.getOrNull()?.authorsText,
|
|
||||||
durationText = song.durationText,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
songs.map(DetailedSong::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
startContent = {
|
|
||||||
BasicText(
|
|
||||||
text = "${index + 1}",
|
|
||||||
style = typography.xs.secondary.bold.center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier
|
|
||||||
.width(36.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
NonQueuedMediaItemMenu(
|
|
||||||
mediaItem = song.asMediaItem,
|
|
||||||
onDismiss = menuState::hide,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoadingOrError(
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = errorMessage,
|
|
||||||
onRetry = onRetry
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = colorPalette.shimmer, shape = ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.album)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
TextPlaceholder()
|
|
||||||
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import it.vfsfitvnm.route.Route0
|
|||||||
import it.vfsfitvnm.route.Route1
|
import it.vfsfitvnm.route.Route1
|
||||||
import it.vfsfitvnm.route.RouteHandlerScope
|
import it.vfsfitvnm.route.RouteHandlerScope
|
||||||
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen
|
||||||
|
|
||||||
val albumRoute = Route1<String?>("albumRoute")
|
val albumRoute = Route1<String?>("albumRoute")
|
||||||
val artistRoute = Route1<String?>("artistRoute")
|
val artistRoute = Route1<String?>("artistRoute")
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SimpleScaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun AlbumScreen(browseId: String) {
|
||||||
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
|
globalRoutes()
|
||||||
|
|
||||||
|
host {
|
||||||
|
SimpleScaffold(
|
||||||
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
|
onTopIconButtonClick = pop,
|
||||||
|
) {
|
||||||
|
AlbumSongList(browseId = browseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.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.thumbnail
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun AlbumSongList(
|
||||||
|
browseId: String,
|
||||||
|
viewModel: AlbumViewModel = viewModel(
|
||||||
|
key = browseId,
|
||||||
|
factory = object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return AlbumViewModel(browseId) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
BoxWithConstraints {
|
||||||
|
val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
|
||||||
|
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||||
|
|
||||||
|
viewModel.result?.getOrNull()?.let { albumWithSongs ->
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Header(title = albumWithSongs.album.title ?: "Unknown") {
|
||||||
|
if (albumWithSongs.songs.isNotEmpty()) {
|
||||||
|
BasicText(
|
||||||
|
text = "Enqueue",
|
||||||
|
style = typography.xxs.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable {
|
||||||
|
binder?.player?.enqueue(
|
||||||
|
albumWithSongs.songs.map(DetailedSong::asMediaItem)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.share_social),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
albumWithSongs.album.shareUrl?.let { url ->
|
||||||
|
val sendIntent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_TEXT, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
sendIntent,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = albumWithSongs.songs,
|
||||||
|
key = { _, song -> song.id }
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
title = song.title,
|
||||||
|
authors = song.artistsText ?: albumWithSongs.album.authorsText,
|
||||||
|
durationText = song.durationText,
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(
|
||||||
|
albumWithSongs.songs.map(DetailedSong::asMediaItem),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
},
|
||||||
|
startContent = {
|
||||||
|
BasicText(
|
||||||
|
text = "${index + 1}",
|
||||||
|
style = typography.m.secondary.secondary.center,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(Dimensions.thumbnails.song)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
menuContent = {
|
||||||
|
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(enabled = albumWithSongs.songs.isNotEmpty()) {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(
|
||||||
|
albumWithSongs.songs
|
||||||
|
.shuffled()
|
||||||
|
.map(DetailedSong::asMediaItem)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.size(62.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.shuffle),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: viewModel.result?.exceptionOrNull()?.let { _ ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures {
|
||||||
|
viewModel.fetch(browseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "An error has occurred.\nTap to retry",
|
||||||
|
style = typography.s.medium.secondary.center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
.shimmer()
|
||||||
|
) {
|
||||||
|
HeaderPlaceholder()
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
.background(colorPalette.shimmer)
|
||||||
|
)
|
||||||
|
|
||||||
|
repeat(3) { index ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(1f - index * 0.25f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
|
||||||
|
.height(Dimensions.thumbnails.song)
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
|
.size(Dimensions.thumbnails.song)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
|
import it.vfsfitvnm.vimusic.models.AlbumWithSongs
|
||||||
|
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
||||||
|
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class AlbumViewModel(browseId: String) : ViewModel() {
|
||||||
|
var result by mutableStateOf<Result<AlbumWithSongs?>?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
fetch(browseId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fetch(browseId: String) {
|
||||||
|
job?.cancel()
|
||||||
|
result = null
|
||||||
|
|
||||||
|
job = viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
Database.albumWithSongs(browseId).collect { albumWithSongs ->
|
||||||
|
result = if (albumWithSongs?.album?.timestamp == null) {
|
||||||
|
YouTube.album(browseId)?.map { youtubeAlbum ->
|
||||||
|
Database.upsert(
|
||||||
|
Album(
|
||||||
|
id = browseId,
|
||||||
|
title = youtubeAlbum.title,
|
||||||
|
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
||||||
|
year = youtubeAlbum.year,
|
||||||
|
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
|
||||||
|
shareUrl = youtubeAlbum.url,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
),
|
||||||
|
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
|
||||||
|
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
|
||||||
|
Database.insert(mediaItem)
|
||||||
|
SongAlbumMap(
|
||||||
|
songId = mediaItem.mediaId,
|
||||||
|
albumId = browseId,
|
||||||
|
position = position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Result.success(albumWithSongs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
object Dimensions {
|
object Dimensions {
|
||||||
val itemsVerticalPadding = 8.dp
|
val itemsVerticalPadding = 8.dp
|
||||||
|
|
||||||
|
val verticalBarWidth = 64.dp
|
||||||
|
|
||||||
object thumbnails {
|
object thumbnails {
|
||||||
val album = 128.dp
|
val album = 128.dp
|
||||||
val artist = 192.dp
|
val artist = 192.dp
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
@@ -158,6 +159,7 @@ fun SongItem(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = Dimensions.itemsVerticalPadding)
|
.padding(vertical = Dimensions.itemsVerticalPadding)
|
||||||
.padding(start = 16.dp, end = if (trailingContent == null) 16.dp else 8.dp)
|
.padding(start = 16.dp, end = if (trailingContent == null) 16.dp else 8.dp)
|
||||||
|
.height(Dimensions.thumbnails.song)
|
||||||
) {
|
) {
|
||||||
startContent()
|
startContent()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user