Drop ViewModel
This commit is contained in:
@@ -84,7 +84,6 @@ dependencies {
|
|||||||
implementation(libs.compose.ripple)
|
implementation(libs.compose.ripple)
|
||||||
implementation(libs.compose.shimmer)
|
implementation(libs.compose.shimmer)
|
||||||
implementation(libs.compose.coil)
|
implementation(libs.compose.coil)
|
||||||
implementation(libs.compose.viewmodel)
|
|
||||||
|
|
||||||
implementation(libs.palette)
|
implementation(libs.palette)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 18,
|
"version": 18,
|
||||||
"identityHash": "dec162db7ec49f4324481d54c49a793d",
|
"identityHash": "c8f776e899b181081f0230bffec99ac5",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "Song",
|
"tableName": "Song",
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "Artist",
|
"tableName": "Artist",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -236,6 +236,12 @@
|
|||||||
"columnName": "timestamp",
|
"columnName": "timestamp",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkedAt",
|
||||||
|
"columnName": "bookmarkedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@@ -318,7 +324,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "Album",
|
"tableName": "Album",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -361,6 +367,12 @@
|
|||||||
"columnName": "timestamp",
|
"columnName": "timestamp",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkedAt",
|
||||||
|
"columnName": "bookmarkedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@@ -588,15 +600,11 @@
|
|||||||
{
|
{
|
||||||
"viewName": "SortedSongPlaylistMap",
|
"viewName": "SortedSongPlaylistMap",
|
||||||
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
|
"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": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dec162db7ec49f4324481d54c49a793d')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c8f776e899b181081f0230bffec99ac5')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,608 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 19,
|
|
||||||
"identityHash": "41479c8284963d3533c4baa46d7464a6",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "Song",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "artistsText",
|
|
||||||
"columnName": "artistsText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "durationText",
|
|
||||||
"columnName": "durationText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnailUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lyrics",
|
|
||||||
"columnName": "lyrics",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "synchronizedLyrics",
|
|
||||||
"columnName": "synchronizedLyrics",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "likedAt",
|
|
||||||
"columnName": "likedAt",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "totalPlayTimeMs",
|
|
||||||
"columnName": "totalPlayTimeMs",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SongPlaylistMap",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "playlistId",
|
|
||||||
"columnName": "playlistId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "position",
|
|
||||||
"columnName": "position",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId",
|
|
||||||
"playlistId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SongPlaylistMap_songId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_SongPlaylistMap_playlistId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"playlistId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "Playlist",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"playlistId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Playlist",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "browseId",
|
|
||||||
"columnName": "browseId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Artist",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnailUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "info",
|
|
||||||
"columnName": "info",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shuffleVideoId",
|
|
||||||
"columnName": "shuffleVideoId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shufflePlaylistId",
|
|
||||||
"columnName": "shufflePlaylistId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "radioVideoId",
|
|
||||||
"columnName": "radioVideoId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "radioPlaylistId",
|
|
||||||
"columnName": "radioPlaylistId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"columnName": "timestamp",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SongArtistMap",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "artistId",
|
|
||||||
"columnName": "artistId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId",
|
|
||||||
"artistId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SongArtistMap_songId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_SongArtistMap_artistId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"artistId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "Artist",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"artistId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Album",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnailUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "year",
|
|
||||||
"columnName": "year",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "authorsText",
|
|
||||||
"columnName": "authorsText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shareUrl",
|
|
||||||
"columnName": "shareUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"columnName": "timestamp",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bookmarkedAt",
|
|
||||||
"columnName": "bookmarkedAt",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SongAlbumMap",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "albumId",
|
|
||||||
"columnName": "albumId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "position",
|
|
||||||
"columnName": "position",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId",
|
|
||||||
"albumId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SongAlbumMap_songId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_SongAlbumMap_albumId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"albumId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "Album",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"albumId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SearchQuery",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "query",
|
|
||||||
"columnName": "query",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SearchQuery_query",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"query"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "QueuedMediaItem",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mediaItem",
|
|
||||||
"columnName": "mediaItem",
|
|
||||||
"affinity": "BLOB",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "position",
|
|
||||||
"columnName": "position",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Format",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "itag",
|
|
||||||
"columnName": "itag",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mimeType",
|
|
||||||
"columnName": "mimeType",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bitrate",
|
|
||||||
"columnName": "bitrate",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contentLength",
|
|
||||||
"columnName": "contentLength",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastModified",
|
|
||||||
"columnName": "lastModified",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "loudnessDb",
|
|
||||||
"columnName": "loudnessDb",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [
|
|
||||||
{
|
|
||||||
"viewName": "SortedSongPlaylistMap",
|
|
||||||
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"viewName": "SortedSongAlbumMap",
|
|
||||||
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '41479c8284963d3533c4baa46d7464a6')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,614 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 20,
|
|
||||||
"identityHash": "821aa30ff7d14b31e839b2f3b2312f78",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "Song",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "artistsText",
|
|
||||||
"columnName": "artistsText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "durationText",
|
|
||||||
"columnName": "durationText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnailUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lyrics",
|
|
||||||
"columnName": "lyrics",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "synchronizedLyrics",
|
|
||||||
"columnName": "synchronizedLyrics",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "likedAt",
|
|
||||||
"columnName": "likedAt",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "totalPlayTimeMs",
|
|
||||||
"columnName": "totalPlayTimeMs",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SongPlaylistMap",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "playlistId",
|
|
||||||
"columnName": "playlistId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "position",
|
|
||||||
"columnName": "position",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId",
|
|
||||||
"playlistId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SongPlaylistMap_songId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_SongPlaylistMap_playlistId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"playlistId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "Playlist",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"playlistId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Playlist",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "browseId",
|
|
||||||
"columnName": "browseId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Artist",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnailUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "info",
|
|
||||||
"columnName": "info",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shuffleVideoId",
|
|
||||||
"columnName": "shuffleVideoId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shufflePlaylistId",
|
|
||||||
"columnName": "shufflePlaylistId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "radioVideoId",
|
|
||||||
"columnName": "radioVideoId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "radioPlaylistId",
|
|
||||||
"columnName": "radioPlaylistId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"columnName": "timestamp",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bookmarkedAt",
|
|
||||||
"columnName": "bookmarkedAt",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SongArtistMap",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "artistId",
|
|
||||||
"columnName": "artistId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId",
|
|
||||||
"artistId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SongArtistMap_songId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_SongArtistMap_artistId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"artistId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "Artist",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"artistId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Album",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "thumbnailUrl",
|
|
||||||
"columnName": "thumbnailUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "year",
|
|
||||||
"columnName": "year",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "authorsText",
|
|
||||||
"columnName": "authorsText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "shareUrl",
|
|
||||||
"columnName": "shareUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"columnName": "timestamp",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bookmarkedAt",
|
|
||||||
"columnName": "bookmarkedAt",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SongAlbumMap",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "albumId",
|
|
||||||
"columnName": "albumId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "position",
|
|
||||||
"columnName": "position",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId",
|
|
||||||
"albumId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SongAlbumMap_songId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "index_SongAlbumMap_albumId",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"albumId"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"table": "Album",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"albumId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SearchQuery",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "query",
|
|
||||||
"columnName": "query",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SearchQuery_query",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"query"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "QueuedMediaItem",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mediaItem",
|
|
||||||
"columnName": "mediaItem",
|
|
||||||
"affinity": "BLOB",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "position",
|
|
||||||
"columnName": "position",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Format",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "songId",
|
|
||||||
"columnName": "songId",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "itag",
|
|
||||||
"columnName": "itag",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "mimeType",
|
|
||||||
"columnName": "mimeType",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "bitrate",
|
|
||||||
"columnName": "bitrate",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contentLength",
|
|
||||||
"columnName": "contentLength",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "lastModified",
|
|
||||||
"columnName": "lastModified",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "loudnessDb",
|
|
||||||
"columnName": "loudnessDb",
|
|
||||||
"affinity": "REAL",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"songId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "Song",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"songId"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [
|
|
||||||
{
|
|
||||||
"viewName": "SortedSongPlaylistMap",
|
|
||||||
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"viewName": "SortedSongAlbumMap",
|
|
||||||
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '821aa30ff7d14b31e839b2f3b2312f78')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,7 +34,6 @@ 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
|
||||||
@@ -48,7 +47,6 @@ 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
|
||||||
@@ -173,9 +171,13 @@ interface Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM Album WHERE id = :id")
|
@Query("SELECT * FROM Album WHERE id = :id")
|
||||||
fun albumWithSongs(id: String): Flow<AlbumWithSongs?>
|
fun album(id: String): Flow<Album?>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
fun albumSongs(albumId: String): Flow<List<DetailedSong>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC")
|
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC")
|
||||||
fun albumsByTitleAsc(): Flow<List<Album>>
|
fun albumsByTitleAsc(): Flow<List<Album>>
|
||||||
@@ -421,10 +423,9 @@ interface Database {
|
|||||||
Format::class,
|
Format::class,
|
||||||
],
|
],
|
||||||
views = [
|
views = [
|
||||||
SortedSongPlaylistMap::class,
|
SortedSongPlaylistMap::class
|
||||||
SortedSongAlbumMap::class
|
|
||||||
],
|
],
|
||||||
version = 20,
|
version = 18,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
@@ -441,8 +442,6 @@ interface Database {
|
|||||||
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),
|
AutoMigration(from = 17, to = 18),
|
||||||
AutoMigration(from = 18, to = 19),
|
|
||||||
AutoMigration(from = 19, to = 20),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.models
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.room.Embedded
|
|
||||||
import androidx.room.Junction
|
|
||||||
import androidx.room.Relation
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class AlbumWithSongs(
|
|
||||||
@Embedded val album: Album,
|
|
||||||
@Relation(
|
|
||||||
entity = Song::class,
|
|
||||||
parentColumn = "id",
|
|
||||||
entityColumn = "id",
|
|
||||||
associateBy = Junction(
|
|
||||||
value = SortedSongAlbumMap::class,
|
|
||||||
parentColumn = "albumId",
|
|
||||||
entityColumn = "songId"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val songs: List<DetailedSong>
|
|
||||||
)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.models
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.DatabaseView
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
@DatabaseView("SELECT * FROM SongAlbumMap ORDER BY position")
|
|
||||||
data class SortedSongAlbumMap(
|
|
||||||
@ColumnInfo(index = true) val songId: String,
|
|
||||||
@ColumnInfo(index = true) val albumId: String,
|
|
||||||
val position: Int
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val AlbumListSaver = ListSaver.of(AlbumSaver)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val AlbumResultSaver = ResultSaver.of(AlbumSaver)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
|
|
||||||
|
object AlbumSaver : Saver<Album, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Album): List<Any?> = listOf(
|
||||||
|
value.id,
|
||||||
|
value.title,
|
||||||
|
value.thumbnailUrl,
|
||||||
|
value.year,
|
||||||
|
value.authorsText,
|
||||||
|
value.shareUrl,
|
||||||
|
value.timestamp,
|
||||||
|
value.bookmarkedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>): Album = Album(
|
||||||
|
id = value[0] as String,
|
||||||
|
title = value[1] as String,
|
||||||
|
thumbnailUrl = value[2] as String?,
|
||||||
|
year = value[3] as String?,
|
||||||
|
authorsText = value[4] as String?,
|
||||||
|
shareUrl = value[5] as String?,
|
||||||
|
timestamp = value[6] as Long?,
|
||||||
|
bookmarkedAt = value[7] as Long?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val ArtistListSaver = ListSaver.of(ArtistSaver)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
|
|
||||||
|
object ArtistSaver : Saver<Artist, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Artist): List<Any?> = listOf(
|
||||||
|
value.id,
|
||||||
|
value.name,
|
||||||
|
value.thumbnailUrl,
|
||||||
|
value.info,
|
||||||
|
value.shuffleVideoId,
|
||||||
|
value.shufflePlaylistId,
|
||||||
|
value.radioVideoId,
|
||||||
|
value.radioPlaylistId,
|
||||||
|
value.timestamp,
|
||||||
|
value.bookmarkedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>): Artist = Artist(
|
||||||
|
id = value[0] as String,
|
||||||
|
name = value[1] as String,
|
||||||
|
thumbnailUrl = value[2] as String?,
|
||||||
|
info = value[3] as String?,
|
||||||
|
shuffleVideoId = value[4] as String?,
|
||||||
|
shufflePlaylistId = value[5] as String?,
|
||||||
|
radioVideoId = value[6] as String?,
|
||||||
|
radioPlaylistId = value[7] as String?,
|
||||||
|
timestamp = value[8] as Long?,
|
||||||
|
bookmarkedAt = value[9] as Long?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val DetailedSongListSaver = ListSaver.of(DetailedSongSaver)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
|
||||||
|
object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: DetailedSong): List<Any?> =
|
||||||
|
listOf(
|
||||||
|
value.id,
|
||||||
|
value.title,
|
||||||
|
value.artistsText,
|
||||||
|
value.durationText,
|
||||||
|
value.thumbnailUrl,
|
||||||
|
value.totalPlayTimeMs,
|
||||||
|
value.albumId,
|
||||||
|
value.artists?.let { with(InfoListSaver) { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>): DetailedSong? {
|
||||||
|
return if (value.size == 8) DetailedSong(
|
||||||
|
id = value[0] as String,
|
||||||
|
title = value[1] as String,
|
||||||
|
artistsText = value[2] as String?,
|
||||||
|
durationText = value[3] as String,
|
||||||
|
thumbnailUrl = value[4] as String?,
|
||||||
|
totalPlayTimeMs = value[5] as Long,
|
||||||
|
albumId = value[6] as String?,
|
||||||
|
artists = InfoListSaver.restore(value[7] as List<List<String>>)
|
||||||
|
) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val InfoListSaver = ListSaver.of(InfoSaver)
|
||||||
16
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt
Normal file
16
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.Info
|
||||||
|
|
||||||
|
object InfoSaver : Saver<Info, List<String>> {
|
||||||
|
override fun SaverScope.save(value: Info): List<String> = listOf(value.id, value.name)
|
||||||
|
|
||||||
|
override fun restore(value: List<String>): Info? {
|
||||||
|
return if (value.size == 2) Info(
|
||||||
|
id = value[0],
|
||||||
|
name = value[1],
|
||||||
|
) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt
Normal file
20
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
|
||||||
|
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
|
||||||
|
companion object {
|
||||||
|
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>): ListSaver<Original, Saveable> {
|
||||||
|
return object : ListSaver<Original, Saveable> {
|
||||||
|
override fun restore(value: List<Saveable>): List<Original> {
|
||||||
|
return value.mapNotNull(saver::restore)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun SaverScope.save(value: List<Original>): List<Saveable> {
|
||||||
|
return with(saver) { value.mapNotNull { save(it) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val PlaylistPreviewListSaver = ListSaver.of(PlaylistPreviewSaver)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
|
|
||||||
|
object PlaylistPreviewSaver : Saver<PlaylistPreview, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: PlaylistPreview): List<Any> {
|
||||||
|
return listOf(
|
||||||
|
with(PlaylistSaver) { save(value.playlist) },
|
||||||
|
value.songCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>): PlaylistPreview? {
|
||||||
|
return if (value.size == 2) PlaylistPreview(
|
||||||
|
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
||||||
|
songCount = value[1] as Int,
|
||||||
|
) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
|
|
||||||
|
object PlaylistSaver : Saver<Playlist, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Playlist): List<Any?> = listOf(
|
||||||
|
value.id,
|
||||||
|
value.name,
|
||||||
|
value.browseId,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>): Playlist = Playlist(
|
||||||
|
id = value[0] as Long,
|
||||||
|
name = value[1] as String,
|
||||||
|
browseId = value[2] as String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
|
||||||
|
interface ResultSaver<Original, Saveable> : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
|
||||||
|
companion object {
|
||||||
|
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>) =
|
||||||
|
object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
|
||||||
|
override fun restore(value: Pair<Saveable?, Throwable?>) =
|
||||||
|
value.first?.let(saver::restore)?.let(Result.Companion::success)
|
||||||
|
?: value.second?.let(Result.Companion::failure)
|
||||||
|
|
||||||
|
override fun SaverScope.save(value: Result<Original>?) =
|
||||||
|
with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val SearchQueryListSaver = ListSaver.of(SearchQuerySaver)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.SearchQuery
|
||||||
|
|
||||||
|
object SearchQuerySaver : Saver<SearchQuery, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: SearchQuery): List<Any?> = listOf(
|
||||||
|
value.id,
|
||||||
|
value.query,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = SearchQuery(
|
||||||
|
id = value[0] as Long,
|
||||||
|
query = value[1] as String
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
|
|
||||||
|
val StringListResultSaver = ResultSaver.of(autoSaver<List<String>?>())
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
|
|
||||||
|
val StringResultSaver = ResultSaver.of(autoSaver<String?>())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val YouTubeAlbumListSaver = ListSaver.of(YouTubeAlbumSaver)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
|
object YouTubeAlbumSaver : Saver<YouTube.Item.Album, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Item.Album): List<Any?> = listOf(
|
||||||
|
with(YouTubeBrowseInfoSaver) { save(value.info) },
|
||||||
|
with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } },
|
||||||
|
value.year,
|
||||||
|
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Item.Album(
|
||||||
|
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
|
||||||
|
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
|
||||||
|
year = value[2] as String?,
|
||||||
|
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val YouTubeArtistListSaver = ListSaver.of(YouTubeArtistSaver)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
|
object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
|
||||||
|
with(YouTubeBrowseInfoSaver) { save(value.info) },
|
||||||
|
value.subscribersCountText,
|
||||||
|
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Item.Artist(
|
||||||
|
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
|
||||||
|
subscribersCountText = value[1] as String?,
|
||||||
|
thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
object YouTubeBrowseEndpointSaver : Saver<NavigationEndpoint.Endpoint.Browse, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf(
|
||||||
|
value.browseId,
|
||||||
|
value.params
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = NavigationEndpoint.Endpoint.Browse(
|
||||||
|
browseId = value[0] as String,
|
||||||
|
params = value[1] as String?,
|
||||||
|
browseEndpointContextSupportedConfigs = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val YouTubeBrowseInfoListSaver = ListSaver.of(YouTubeBrowseInfoSaver)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
object YouTubeBrowseInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
|
||||||
|
value.name,
|
||||||
|
with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Info(
|
||||||
|
name = value[0] as String,
|
||||||
|
endpoint = (value[1] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val YouTubePlaylistListSaver = ListSaver.of(YouTubePlaylistSaver)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
|
object YouTubePlaylistSaver : Saver<YouTube.Item.Playlist, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Item.Playlist): List<Any?> = listOf(
|
||||||
|
with(YouTubeBrowseInfoSaver) { save(value.info) },
|
||||||
|
with(YouTubeBrowseInfoSaver) { value.channel?.let { save(it) } },
|
||||||
|
value.songCount,
|
||||||
|
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Item.Playlist(
|
||||||
|
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
|
||||||
|
channel = (value[1] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
|
||||||
|
songCount = value[2] as Int?,
|
||||||
|
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val YouTubeSongListSaver = ListSaver.of(YouTubeSongSaver)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
|
object YouTubeSongSaver : Saver<YouTube.Item.Song, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Item.Song): List<Any?> = listOf(
|
||||||
|
with(YouTubeWatchInfoSaver) { save(value.info) },
|
||||||
|
with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } },
|
||||||
|
with(YouTubeBrowseInfoSaver) { value.album?.let { save(it) } },
|
||||||
|
value.durationText,
|
||||||
|
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Item.Song(
|
||||||
|
info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
|
||||||
|
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
|
||||||
|
album = (value[2] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
|
||||||
|
durationText = value[3] as String?,
|
||||||
|
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
|
||||||
|
|
||||||
|
object YouTubeThumbnailSaver : Saver<ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail) = listOf(
|
||||||
|
value.url,
|
||||||
|
value.width,
|
||||||
|
value.height
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail(
|
||||||
|
url = value[0] as String,
|
||||||
|
width = value[1] as Int,
|
||||||
|
height = value[2] as Int?,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val YouTubeVideoListSaver = ListSaver.of(YouTubeVideoSaver)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
|
object YouTubeVideoSaver : Saver<YouTube.Item.Video, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Item.Video): List<Any?> = listOf(
|
||||||
|
with(YouTubeWatchInfoSaver) { save(value.info) },
|
||||||
|
with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } },
|
||||||
|
value.viewsText,
|
||||||
|
value.durationText,
|
||||||
|
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Item.Video(
|
||||||
|
info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
|
||||||
|
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
|
||||||
|
viewsText = value[2] as String?,
|
||||||
|
durationText = value[3] as String?,
|
||||||
|
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
object YouTubeWatchEndpointSaver : Saver<NavigationEndpoint.Endpoint.Watch, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf(
|
||||||
|
value.params,
|
||||||
|
value.playlistId,
|
||||||
|
value.videoId,
|
||||||
|
value.index,
|
||||||
|
value.playlistSetVideoId,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = NavigationEndpoint.Endpoint.Watch(
|
||||||
|
params = value[0] as String?,
|
||||||
|
playlistId = value[1] as String?,
|
||||||
|
videoId = value[2] as String?,
|
||||||
|
index = value[3] as Int?,
|
||||||
|
playlistSetVideoId = value[4] as String?,
|
||||||
|
watchEndpointMusicSupportedConfigs = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
object YouTubeWatchInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
|
||||||
|
value.name,
|
||||||
|
with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.Info(
|
||||||
|
name = value[0] as String,
|
||||||
|
endpoint = (value[1] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ 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
|
import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen
|
||||||
|
|
||||||
val albumRoute = Route1<String?>("albumRoute")
|
val albumRoute = Route1<String?>("albumRoute")
|
||||||
val artistRoute = Route1<String?>("artistRoute")
|
val artistRoute = Route1<String?>("artistRoute")
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
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.getValue
|
||||||
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.alpha
|
||||||
@@ -34,17 +35,16 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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 androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
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.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.Album
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.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.NonQueuedMediaItemMenu
|
||||||
@@ -61,6 +61,7 @@ import it.vfsfitvnm.vimusic.utils.enqueue
|
|||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableListState
|
||||||
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
|
||||||
@@ -69,26 +70,26 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
|
|||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun AlbumOverview(
|
fun AlbumOverview(
|
||||||
|
albumResult: Result<Album>?,
|
||||||
browseId: String,
|
browseId: String,
|
||||||
viewModel: AlbumOverviewViewModel = viewModel(
|
|
||||||
key = browseId,
|
|
||||||
factory = object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return AlbumOverviewViewModel(browseId) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val songs by produceSaveableListState(
|
||||||
|
flowProvider = {
|
||||||
|
Database.albumSongs(browseId)
|
||||||
|
},
|
||||||
|
stateSaver = DetailedSongListSaver
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
|
val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
|
||||||
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||||
|
|
||||||
viewModel.result?.getOrNull()?.let { albumWithSongs ->
|
albumResult?.getOrNull()?.let { album ->
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -100,8 +101,8 @@ fun AlbumOverview(
|
|||||||
contentType = 0
|
contentType = 0
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Header(title = albumWithSongs.album.title ?: "Unknown") {
|
Header(title = album.title ?: "Unknown") {
|
||||||
if (albumWithSongs.songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Enqueue",
|
text = "Enqueue",
|
||||||
style = typography.xxs.medium,
|
style = typography.xxs.medium,
|
||||||
@@ -109,7 +110,7 @@ fun AlbumOverview(
|
|||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
binder?.player?.enqueue(
|
binder?.player?.enqueue(
|
||||||
albumWithSongs.songs.map(DetailedSong::asMediaItem)
|
songs.map(DetailedSong::asMediaItem)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.background(colorPalette.background2)
|
.background(colorPalette.background2)
|
||||||
@@ -125,7 +126,7 @@ fun AlbumOverview(
|
|||||||
|
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
if (albumWithSongs.album.bookmarkedAt == null) {
|
if (album.bookmarkedAt == null) {
|
||||||
R.drawable.bookmark_outline
|
R.drawable.bookmark_outline
|
||||||
} else {
|
} else {
|
||||||
R.drawable.bookmark
|
R.drawable.bookmark
|
||||||
@@ -137,8 +138,8 @@ fun AlbumOverview(
|
|||||||
.clickable {
|
.clickable {
|
||||||
query {
|
query {
|
||||||
Database.update(
|
Database.update(
|
||||||
albumWithSongs.album.copy(
|
album.copy(
|
||||||
bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) {
|
bookmarkedAt = if (album.bookmarkedAt == null) {
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -157,7 +158,7 @@ fun AlbumOverview(
|
|||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
albumWithSongs.album.shareUrl?.let { url ->
|
album.shareUrl?.let { url ->
|
||||||
val sendIntent = Intent().apply {
|
val sendIntent = Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
@@ -178,7 +179,7 @@ fun AlbumOverview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
model = album.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
@@ -190,17 +191,17 @@ fun AlbumOverview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = albumWithSongs.songs,
|
items = songs,
|
||||||
key = { _, song -> song.id }
|
key = { _, song -> song.id }
|
||||||
) { index, song ->
|
) { index, song ->
|
||||||
SongItem(
|
SongItem(
|
||||||
title = song.title,
|
title = song.title,
|
||||||
authors = song.artistsText ?: albumWithSongs.album.authorsText,
|
authors = song.artistsText ?: album.authorsText,
|
||||||
durationText = song.durationText,
|
durationText = song.durationText,
|
||||||
onClick = {
|
onClick = {
|
||||||
binder?.stopRadio()
|
binder?.stopRadio()
|
||||||
binder?.player?.forcePlayAtIndex(
|
binder?.player?.forcePlayAtIndex(
|
||||||
albumWithSongs.songs.map(DetailedSong::asMediaItem),
|
songs.map(DetailedSong::asMediaItem),
|
||||||
index
|
index
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -227,10 +228,10 @@ fun AlbumOverview(
|
|||||||
.padding(all = 16.dp)
|
.padding(all = 16.dp)
|
||||||
.padding(LocalPlayerAwarePaddingValues.current)
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.clickable(enabled = albumWithSongs.songs.isNotEmpty()) {
|
.clickable(enabled = songs.isNotEmpty()) {
|
||||||
binder?.stopRadio()
|
binder?.stopRadio()
|
||||||
binder?.player?.forcePlayFromBeginning(
|
binder?.player?.forcePlayFromBeginning(
|
||||||
albumWithSongs.songs
|
songs
|
||||||
.shuffled()
|
.shuffled()
|
||||||
.map(DetailedSong::asMediaItem)
|
.map(DetailedSong::asMediaItem)
|
||||||
)
|
)
|
||||||
@@ -247,12 +248,12 @@ fun AlbumOverview(
|
|||||||
.size(20.dp)
|
.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} ?: viewModel.result?.exceptionOrNull()?.let {
|
} ?: albumResult?.exceptionOrNull()?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures {
|
detectTapGestures {
|
||||||
viewModel.fetch(browseId)
|
// viewModel.fetch(browseId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.album
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.models.Album
|
|
||||||
import it.vfsfitvnm.vimusic.models.AlbumWithSongs
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class AlbumOverviewViewModel(browseId: String) : ViewModel() {
|
|
||||||
var result by mutableStateOf<Result<AlbumWithSongs?>?>(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
fetch(browseId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetch(browseId: String) {
|
|
||||||
job?.cancel()
|
|
||||||
result = null
|
|
||||||
|
|
||||||
job = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
Database.albumWithSongs(browseId).collect { albumWithSongs ->
|
|
||||||
result = if (albumWithSongs?.album?.timestamp == null) {
|
|
||||||
YouTube.album(browseId)?.map { youtubeAlbum ->
|
|
||||||
Database.upsert(
|
|
||||||
Album(
|
|
||||||
id = browseId,
|
|
||||||
title = youtubeAlbum.title,
|
|
||||||
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
|
||||||
year = youtubeAlbum.year,
|
|
||||||
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
|
|
||||||
shareUrl = youtubeAlbum.url,
|
|
||||||
timestamp = System.currentTimeMillis()
|
|
||||||
),
|
|
||||||
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
|
|
||||||
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
SongAlbumMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
albumId = browseId,
|
|
||||||
position = position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Result.success(albumWithSongs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,21 @@ package it.vfsfitvnm.vimusic.ui.screens.album
|
|||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
|
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
||||||
|
import it.vfsfitvnm.vimusic.savers.AlbumResultSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@@ -19,6 +29,45 @@ fun AlbumScreen(browseId: String) {
|
|||||||
globalRoutes()
|
globalRoutes()
|
||||||
|
|
||||||
host {
|
host {
|
||||||
|
val albumResult by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = AlbumResultSaver,
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Database.album(browseId).collect { album ->
|
||||||
|
if (album?.timestamp == null) {
|
||||||
|
YouTube.album(browseId)?.map { youtubeAlbum ->
|
||||||
|
Database.upsert(
|
||||||
|
Album(
|
||||||
|
id = browseId,
|
||||||
|
title = youtubeAlbum.title,
|
||||||
|
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
||||||
|
year = youtubeAlbum.year,
|
||||||
|
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
|
||||||
|
shareUrl = youtubeAlbum.url,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
),
|
||||||
|
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
|
||||||
|
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
|
||||||
|
Database.insert(mediaItem)
|
||||||
|
SongAlbumMap(
|
||||||
|
songId = mediaItem.mediaId,
|
||||||
|
albumId = browseId,
|
||||||
|
position = position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = Result.success(album)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topIconButtonId = R.drawable.chevron_back,
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
onTopIconButtonClick = pop,
|
onTopIconButtonClick = pop,
|
||||||
@@ -29,7 +78,10 @@ fun AlbumScreen(browseId: String) {
|
|||||||
}
|
}
|
||||||
) { currentTabIndex ->
|
) { currentTabIndex ->
|
||||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
AlbumOverview(browseId = browseId)
|
AlbumOverview(
|
||||||
|
albumResult = albumResult,
|
||||||
|
browseId = browseId,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
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.Database
|
||||||
@@ -70,239 +69,230 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
|
|||||||
@Composable
|
@Composable
|
||||||
fun ArtistOverview(
|
fun ArtistOverview(
|
||||||
browseId: String,
|
browseId: String,
|
||||||
viewModel: ArtistOverviewViewModel = viewModel(
|
|
||||||
key = browseId,
|
|
||||||
factory = object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return ArtistOverviewViewModel(browseId) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
BoxWithConstraints {
|
// BoxWithConstraints {
|
||||||
val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
|
// val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
|
||||||
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
// val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||||
|
//
|
||||||
viewModel.result?.getOrNull()?.let { albumWithSongs ->
|
// viewModel.result?.getOrNull()?.let { albumWithSongs ->
|
||||||
LazyColumn(
|
// LazyColumn(
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
// contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.background(colorPalette.background0)
|
// .background(colorPalette.background0)
|
||||||
.fillMaxSize()
|
// .fillMaxSize()
|
||||||
) {
|
// ) {
|
||||||
item(
|
// item(
|
||||||
key = "header",
|
// key = "header",
|
||||||
contentType = 0
|
// contentType = 0
|
||||||
) {
|
// ) {
|
||||||
Column {
|
// Column {
|
||||||
Header(title = albumWithSongs.album.title ?: "Unknown") {
|
// Header(title = albumWithSongs.album.title ?: "Unknown") {
|
||||||
if (albumWithSongs.songs.isNotEmpty()) {
|
// if (albumWithSongs.songs.isNotEmpty()) {
|
||||||
BasicText(
|
// BasicText(
|
||||||
text = "Enqueue",
|
// text = "Enqueue",
|
||||||
style = typography.xxs.medium,
|
// style = typography.xxs.medium,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
// .clip(RoundedCornerShape(16.dp))
|
||||||
.clickable {
|
// .clickable {
|
||||||
binder?.player?.enqueue(
|
// binder?.player?.enqueue(
|
||||||
albumWithSongs.songs.map(DetailedSong::asMediaItem)
|
// albumWithSongs.songs.map(DetailedSong::asMediaItem)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
.background(colorPalette.background2)
|
// .background(colorPalette.background2)
|
||||||
.padding(all = 8.dp)
|
// .padding(all = 8.dp)
|
||||||
.padding(horizontal = 8.dp)
|
// .padding(horizontal = 8.dp)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Spacer(
|
// Spacer(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.weight(1f)
|
// .weight(1f)
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
Image(
|
// Image(
|
||||||
painter = painterResource(
|
// painter = painterResource(
|
||||||
if (albumWithSongs.album.bookmarkedAt == null) {
|
// if (albumWithSongs.album.bookmarkedAt == null) {
|
||||||
R.drawable.bookmark_outline
|
// R.drawable.bookmark_outline
|
||||||
} else {
|
// } else {
|
||||||
R.drawable.bookmark
|
// R.drawable.bookmark
|
||||||
}
|
// }
|
||||||
),
|
// ),
|
||||||
contentDescription = null,
|
// contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.accent),
|
// colorFilter = ColorFilter.tint(colorPalette.accent),
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.clickable {
|
// .clickable {
|
||||||
query {
|
// query {
|
||||||
Database.update(
|
// Database.update(
|
||||||
albumWithSongs.album.copy(
|
// albumWithSongs.album.copy(
|
||||||
bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) {
|
// bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) {
|
||||||
System.currentTimeMillis()
|
// System.currentTimeMillis()
|
||||||
} else {
|
// } else {
|
||||||
null
|
// null
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.padding(all = 4.dp)
|
// .padding(all = 4.dp)
|
||||||
.size(18.dp)
|
// .size(18.dp)
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
Image(
|
// Image(
|
||||||
painter = painterResource(R.drawable.share_social),
|
// painter = painterResource(R.drawable.share_social),
|
||||||
contentDescription = null,
|
// contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
// colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.clickable {
|
// .clickable {
|
||||||
albumWithSongs.album.shareUrl?.let { url ->
|
// albumWithSongs.album.shareUrl?.let { url ->
|
||||||
val sendIntent = Intent().apply {
|
// val sendIntent = Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
// action = Intent.ACTION_SEND
|
||||||
type = "text/plain"
|
// type = "text/plain"
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
// putExtra(Intent.EXTRA_TEXT, url)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
context.startActivity(
|
// context.startActivity(
|
||||||
Intent.createChooser(
|
// Intent.createChooser(
|
||||||
sendIntent,
|
// sendIntent,
|
||||||
null
|
// null
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.padding(all = 4.dp)
|
// .padding(all = 4.dp)
|
||||||
.size(18.dp)
|
// .size(18.dp)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
AsyncImage(
|
// AsyncImage(
|
||||||
model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
// model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
contentDescription = null,
|
// contentDescription = null,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally)
|
// .align(Alignment.CenterHorizontally)
|
||||||
.padding(all = 16.dp)
|
// .padding(all = 16.dp)
|
||||||
.clip(thumbnailShape)
|
// .clip(thumbnailShape)
|
||||||
.size(thumbnailSizeDp)
|
// .size(thumbnailSizeDp)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
itemsIndexed(
|
// itemsIndexed(
|
||||||
items = albumWithSongs.songs,
|
// items = albumWithSongs.songs,
|
||||||
key = { _, song -> song.id }
|
// key = { _, song -> song.id }
|
||||||
) { index, song ->
|
// ) { index, song ->
|
||||||
SongItem(
|
// SongItem(
|
||||||
title = song.title,
|
// title = song.title,
|
||||||
authors = song.artistsText ?: albumWithSongs.album.authorsText,
|
// authors = song.artistsText ?: albumWithSongs.album.authorsText,
|
||||||
durationText = song.durationText,
|
// durationText = song.durationText,
|
||||||
onClick = {
|
// onClick = {
|
||||||
binder?.stopRadio()
|
// binder?.stopRadio()
|
||||||
binder?.player?.forcePlayAtIndex(
|
// binder?.player?.forcePlayAtIndex(
|
||||||
albumWithSongs.songs.map(DetailedSong::asMediaItem),
|
// albumWithSongs.songs.map(DetailedSong::asMediaItem),
|
||||||
index
|
// index
|
||||||
)
|
// )
|
||||||
},
|
// },
|
||||||
startContent = {
|
// startContent = {
|
||||||
BasicText(
|
// BasicText(
|
||||||
text = "${index + 1}",
|
// text = "${index + 1}",
|
||||||
style = typography.s.semiBold.center.color(colorPalette.textDisabled),
|
// style = typography.s.semiBold.center.color(colorPalette.textDisabled),
|
||||||
maxLines = 1,
|
// maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
// overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.width(Dimensions.thumbnails.song)
|
// .width(Dimensions.thumbnails.song)
|
||||||
)
|
// )
|
||||||
},
|
// },
|
||||||
menuContent = {
|
// menuContent = {
|
||||||
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
// NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
||||||
}
|
// }
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Box(
|
// Box(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
// .align(Alignment.BottomEnd)
|
||||||
.padding(all = 16.dp)
|
// .padding(all = 16.dp)
|
||||||
.padding(LocalPlayerAwarePaddingValues.current)
|
// .padding(LocalPlayerAwarePaddingValues.current)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
// .clip(RoundedCornerShape(16.dp))
|
||||||
.clickable(enabled = albumWithSongs.songs.isNotEmpty()) {
|
// .clickable(enabled = albumWithSongs.songs.isNotEmpty()) {
|
||||||
binder?.stopRadio()
|
// binder?.stopRadio()
|
||||||
binder?.player?.forcePlayFromBeginning(
|
// binder?.player?.forcePlayFromBeginning(
|
||||||
albumWithSongs.songs
|
// albumWithSongs.songs
|
||||||
.shuffled()
|
// .shuffled()
|
||||||
.map(DetailedSong::asMediaItem)
|
// .map(DetailedSong::asMediaItem)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
.background(colorPalette.background2)
|
// .background(colorPalette.background2)
|
||||||
.size(62.dp)
|
// .size(62.dp)
|
||||||
) {
|
// ) {
|
||||||
Image(
|
// Image(
|
||||||
painter = painterResource(R.drawable.shuffle),
|
// painter = painterResource(R.drawable.shuffle),
|
||||||
contentDescription = null,
|
// contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
// colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.align(Alignment.Center)
|
// .align(Alignment.Center)
|
||||||
.size(20.dp)
|
// .size(20.dp)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
} ?: viewModel.result?.exceptionOrNull()?.let {
|
// } ?: viewModel.result?.exceptionOrNull()?.let {
|
||||||
Box(
|
// Box(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.pointerInput(Unit) {
|
// .pointerInput(Unit) {
|
||||||
detectTapGestures {
|
// detectTapGestures {
|
||||||
viewModel.fetch(browseId)
|
// viewModel.fetch(browseId)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
.align(Alignment.Center)
|
// .align(Alignment.Center)
|
||||||
.fillMaxSize()
|
// .fillMaxSize()
|
||||||
) {
|
// ) {
|
||||||
BasicText(
|
// BasicText(
|
||||||
text = "An error has occurred.\nTap to retry",
|
// text = "An error has occurred.\nTap to retry",
|
||||||
style = typography.s.medium.secondary.center,
|
// style = typography.s.medium.secondary.center,
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.align(Alignment.Center)
|
// .align(Alignment.Center)
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
} ?: Column(
|
// } ?: Column(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.padding(LocalPlayerAwarePaddingValues.current)
|
// .padding(LocalPlayerAwarePaddingValues.current)
|
||||||
.shimmer()
|
// .shimmer()
|
||||||
) {
|
// ) {
|
||||||
HeaderPlaceholder()
|
// HeaderPlaceholder()
|
||||||
|
//
|
||||||
Spacer(
|
// Spacer(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally)
|
// .align(Alignment.CenterHorizontally)
|
||||||
.padding(all = 16.dp)
|
// .padding(all = 16.dp)
|
||||||
.clip(thumbnailShape)
|
// .clip(thumbnailShape)
|
||||||
.size(thumbnailSizeDp)
|
// .size(thumbnailSizeDp)
|
||||||
.background(colorPalette.shimmer)
|
// .background(colorPalette.shimmer)
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
repeat(3) { index ->
|
// repeat(3) { index ->
|
||||||
Row(
|
// Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
// verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
// horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.alpha(1f - index * 0.25f)
|
// .alpha(1f - index * 0.25f)
|
||||||
.fillMaxWidth()
|
// .fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
|
// .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
|
||||||
.height(Dimensions.thumbnails.song)
|
// .height(Dimensions.thumbnails.song)
|
||||||
) {
|
// ) {
|
||||||
Spacer(
|
// Spacer(
|
||||||
modifier = Modifier
|
// modifier = Modifier
|
||||||
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
// .background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
.size(Dimensions.thumbnails.song)
|
// .size(Dimensions.thumbnails.song)
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
Column {
|
// Column {
|
||||||
TextPlaceholder()
|
// TextPlaceholder()
|
||||||
TextPlaceholder()
|
// TextPlaceholder()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.artist
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.models.Album
|
|
||||||
import it.vfsfitvnm.vimusic.models.AlbumWithSongs
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ArtistOverviewViewModel(browseId: String) : ViewModel() {
|
|
||||||
var result by mutableStateOf<Result<AlbumWithSongs?>?>(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
fetch(browseId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetch(browseId: String) {
|
|
||||||
job?.cancel()
|
|
||||||
result = null
|
|
||||||
|
|
||||||
job = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
Database.albumWithSongs(browseId).collect { albumWithSongs ->
|
|
||||||
result = if (albumWithSongs?.album?.timestamp == null) {
|
|
||||||
YouTube.album(browseId)?.map { youtubeAlbum ->
|
|
||||||
Database.upsert(
|
|
||||||
Album(
|
|
||||||
id = browseId,
|
|
||||||
title = youtubeAlbum.title,
|
|
||||||
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
|
||||||
year = youtubeAlbum.year,
|
|
||||||
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
|
|
||||||
shareUrl = youtubeAlbum.url,
|
|
||||||
timestamp = System.currentTimeMillis()
|
|
||||||
),
|
|
||||||
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
|
|
||||||
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
SongAlbumMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
albumId = browseId,
|
|
||||||
position = position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Result.success(albumWithSongs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -80,7 +80,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun AlbumScreen(browseId: String) {
|
fun ArtistScreen(browseId: String) {
|
||||||
val saveableStateHolder = rememberSaveableStateHolder()
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
val (tabIndex, onTabIndexChanged) = rememberSaveable {
|
val (tabIndex, onTabIndexChanged) = rememberSaveable {
|
||||||
mutableStateOf(0)
|
mutableStateOf(0)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import androidx.compose.material.ripple.rememberRipple
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -35,17 +36,22 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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 androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.AlbumSortBy
|
import it.vfsfitvnm.vimusic.enums.AlbumSortBy
|
||||||
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.savers.AlbumListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
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.utils.albumSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.albumSortOrderKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableListState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.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
|
||||||
@@ -54,16 +60,25 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
|
|||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeAlbumList(
|
fun HomeAlbumList(
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit
|
||||||
viewModel: HomeAlbumListViewModel = viewModel()
|
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableListState(
|
||||||
|
flowProvider = { Database.albums(sortBy, sortOrder) },
|
||||||
|
stateSaver = AlbumListSaver,
|
||||||
|
key1 = sortBy,
|
||||||
|
key2 = sortOrder
|
||||||
|
)
|
||||||
|
|
||||||
val thumbnailSizeDp = Dimensions.thumbnails.song * 2
|
val thumbnailSizeDp = Dimensions.thumbnails.song * 2
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
val sortOrderIconRotation by animateFloatAsState(
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,14 +98,14 @@ fun HomeAlbumList(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Item(
|
fun Item(
|
||||||
@DrawableRes iconId: Int,
|
@DrawableRes iconId: Int,
|
||||||
sortBy: AlbumSortBy
|
targetSortBy: AlbumSortBy
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(iconId),
|
painter = painterResource(iconId),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortBy = sortBy }
|
.clickable { sortBy = targetSortBy }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
)
|
)
|
||||||
@@ -98,17 +113,17 @@ fun HomeAlbumList(
|
|||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.calendar,
|
iconId = R.drawable.calendar,
|
||||||
sortBy = AlbumSortBy.Year
|
targetSortBy = AlbumSortBy.Year
|
||||||
)
|
)
|
||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.text,
|
iconId = R.drawable.text,
|
||||||
sortBy = AlbumSortBy.Title
|
targetSortBy = AlbumSortBy.Title
|
||||||
)
|
)
|
||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.time,
|
iconId = R.drawable.time,
|
||||||
sortBy = AlbumSortBy.DateAdded
|
targetSortBy = AlbumSortBy.DateAdded
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
@@ -121,7 +136,7 @@ fun HomeAlbumList(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
.clickable { sortOrder = !sortOrder }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
@@ -130,7 +145,7 @@ fun HomeAlbumList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = viewModel.items,
|
items = items,
|
||||||
key = Album::id
|
key = Album::id
|
||||||
) { album ->
|
) { album ->
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.home
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.enums.AlbumSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
|
||||||
import it.vfsfitvnm.vimusic.models.Album
|
|
||||||
import it.vfsfitvnm.vimusic.utils.albumSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.albumSortOrderKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
|
||||||
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
|
||||||
import it.vfsfitvnm.vimusic.utils.putEnum
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class HomeAlbumListViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
var items by mutableStateOf(emptyList<Album>())
|
|
||||||
private set
|
|
||||||
|
|
||||||
var sortBy by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
albumSortByKey,
|
|
||||||
AlbumSortBy.DateAdded
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(albumSortByKey, it) }
|
|
||||||
collectItems(sortBy = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortOrder by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
albumSortOrderKey,
|
|
||||||
SortOrder.Ascending
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(albumSortOrderKey, it) }
|
|
||||||
collectItems(sortOrder = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences
|
|
||||||
get() = getApplication<Application>().preferences
|
|
||||||
|
|
||||||
init {
|
|
||||||
collectItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectItems(sortBy: AlbumSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
|
|
||||||
job?.cancel()
|
|
||||||
job = viewModelScope.launch {
|
|
||||||
Database.albums(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
|
||||||
items = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,7 @@ import androidx.compose.material.ripple.rememberRipple
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -37,18 +38,23 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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 androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ArtistSortBy
|
import it.vfsfitvnm.vimusic.enums.ArtistSortBy
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
import it.vfsfitvnm.vimusic.models.Artist
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
|
import it.vfsfitvnm.vimusic.savers.ArtistListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
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.utils.artistSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.artistSortOrderKey
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableListState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
|
||||||
@@ -56,16 +62,25 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
|
|||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeArtistList(
|
fun HomeArtistList(
|
||||||
onArtistClick: (Artist) -> Unit,
|
onArtistClick: (Artist) -> Unit
|
||||||
viewModel: HomeArtistListViewModel = viewModel()
|
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableListState(
|
||||||
|
flowProvider = { Database.artists(sortBy, sortOrder) },
|
||||||
|
stateSaver = ArtistListSaver,
|
||||||
|
key1 = sortBy,
|
||||||
|
key2 = sortOrder
|
||||||
|
)
|
||||||
|
|
||||||
val thumbnailSizeDp = Dimensions.thumbnails.song * 2
|
val thumbnailSizeDp = Dimensions.thumbnails.song * 2
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
val sortOrderIconRotation by animateFloatAsState(
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,14 +107,14 @@ fun HomeArtistList(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Item(
|
fun Item(
|
||||||
@DrawableRes iconId: Int,
|
@DrawableRes iconId: Int,
|
||||||
sortBy: ArtistSortBy
|
targetSortBy: ArtistSortBy
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(iconId),
|
painter = painterResource(iconId),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortBy = sortBy }
|
.clickable { sortBy = targetSortBy }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
)
|
)
|
||||||
@@ -107,12 +122,12 @@ fun HomeArtistList(
|
|||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.text,
|
iconId = R.drawable.text,
|
||||||
sortBy = ArtistSortBy.Name
|
targetSortBy = ArtistSortBy.Name
|
||||||
)
|
)
|
||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.time,
|
iconId = R.drawable.time,
|
||||||
sortBy = ArtistSortBy.DateAdded
|
targetSortBy = ArtistSortBy.DateAdded
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
@@ -125,7 +140,7 @@ fun HomeArtistList(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
.clickable { sortOrder = !sortOrder }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
@@ -134,7 +149,7 @@ fun HomeArtistList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = viewModel.items,
|
items = items,
|
||||||
key = Artist::id
|
key = Artist::id
|
||||||
) { artist ->
|
) { artist ->
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.home
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.enums.ArtistSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
|
||||||
import it.vfsfitvnm.vimusic.models.Artist
|
|
||||||
import it.vfsfitvnm.vimusic.utils.artistSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.artistSortOrderKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
|
||||||
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
|
||||||
import it.vfsfitvnm.vimusic.utils.putEnum
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class HomeArtistListViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
var items by mutableStateOf(emptyList<Artist>())
|
|
||||||
private set
|
|
||||||
|
|
||||||
var sortBy by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
artistSortByKey,
|
|
||||||
ArtistSortBy.DateAdded
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(artistSortByKey, it) }
|
|
||||||
collectItems(sortBy = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortOrder by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
artistSortOrderKey,
|
|
||||||
SortOrder.Ascending
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(artistSortOrderKey, it) }
|
|
||||||
collectItems(sortOrder = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences
|
|
||||||
get() = getApplication<Application>().preferences
|
|
||||||
|
|
||||||
init {
|
|
||||||
collectItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectItems(sortBy: ArtistSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
|
|
||||||
job?.cancel()
|
|
||||||
job = viewModelScope.launch {
|
|
||||||
Database.artists(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
|
||||||
items = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,6 @@ import androidx.compose.ui.graphics.ColorFilter
|
|||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
@@ -44,6 +43,7 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
|||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
@@ -51,11 +51,14 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|||||||
import it.vfsfitvnm.vimusic.ui.views.BuiltInPlaylistItem
|
import it.vfsfitvnm.vimusic.ui.views.BuiltInPlaylistItem
|
||||||
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
|
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableListState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePlaylistList(
|
fun HomePlaylistList(
|
||||||
viewModel: HomePlaylistListViewModel = viewModel(),
|
|
||||||
onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
|
onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
|
||||||
onPlaylistClicked: (Playlist) -> Unit,
|
onPlaylistClicked: (Playlist) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -79,8 +82,18 @@ fun HomePlaylistList(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableListState(
|
||||||
|
flowProvider = { Database.playlistPreviews(sortBy, sortOrder) },
|
||||||
|
stateSaver = PlaylistPreviewListSaver,
|
||||||
|
key1 = sortBy,
|
||||||
|
key2 = sortOrder
|
||||||
|
)
|
||||||
|
|
||||||
val sortOrderIconRotation by animateFloatAsState(
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,14 +118,14 @@ fun HomePlaylistList(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Item(
|
fun Item(
|
||||||
@DrawableRes iconId: Int,
|
@DrawableRes iconId: Int,
|
||||||
sortBy: PlaylistSortBy
|
targetSortBy: PlaylistSortBy
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(iconId),
|
painter = painterResource(iconId),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortBy = sortBy }
|
.clickable { sortBy = targetSortBy }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
)
|
)
|
||||||
@@ -136,17 +149,17 @@ fun HomePlaylistList(
|
|||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.medical,
|
iconId = R.drawable.medical,
|
||||||
sortBy = PlaylistSortBy.SongCount
|
targetSortBy = PlaylistSortBy.SongCount
|
||||||
)
|
)
|
||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.text,
|
iconId = R.drawable.text,
|
||||||
sortBy = PlaylistSortBy.Name
|
targetSortBy = PlaylistSortBy.Name
|
||||||
)
|
)
|
||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.time,
|
iconId = R.drawable.time,
|
||||||
sortBy = PlaylistSortBy.DateAdded
|
targetSortBy = PlaylistSortBy.DateAdded
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
@@ -159,7 +172,7 @@ fun HomePlaylistList(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
.clickable { sortOrder = !sortOrder }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
@@ -197,7 +210,7 @@ fun HomePlaylistList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = viewModel.items,
|
items = items,
|
||||||
key = { it.playlist.id }
|
key = { it.playlist.id }
|
||||||
) { playlistPreview ->
|
) { playlistPreview ->
|
||||||
PlaylistPreviewItem(
|
PlaylistPreviewItem(
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.home
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
|
||||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
|
||||||
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
|
||||||
import it.vfsfitvnm.vimusic.utils.putEnum
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class HomePlaylistListViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
var items by mutableStateOf(emptyList<PlaylistPreview>())
|
|
||||||
private set
|
|
||||||
|
|
||||||
var sortBy by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
playlistSortByKey,
|
|
||||||
PlaylistSortBy.DateAdded
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(playlistSortByKey, it) }
|
|
||||||
collectItems(sortBy = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortOrder by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
playlistSortOrderKey,
|
|
||||||
SortOrder.Ascending
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(playlistSortOrderKey, it) }
|
|
||||||
collectItems(sortOrder = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences
|
|
||||||
get() = getApplication<Application>().preferences
|
|
||||||
|
|
||||||
init {
|
|
||||||
collectItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectItems(
|
|
||||||
sortBy: PlaylistSortBy = this.sortBy,
|
|
||||||
sortOrder: SortOrder = this.sortOrder
|
|
||||||
) {
|
|
||||||
job?.cancel()
|
|
||||||
job = viewModelScope.launch {
|
|
||||||
Database.playlistPreviews(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
|
||||||
items = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||||||
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.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
@@ -32,7 +33,7 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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 androidx.lifecycle.viewmodel.compose.viewModel
|
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
|
||||||
@@ -40,6 +41,7 @@ import it.vfsfitvnm.vimusic.enums.SongSortBy
|
|||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
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.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
@@ -50,21 +52,55 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|||||||
import it.vfsfitvnm.vimusic.utils.center
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableListState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeSongList(
|
fun HomeSongList() {
|
||||||
viewModel: HomeSongListViewModel = viewModel()
|
println("[${System.currentTimeMillis()}] HomeSongList")
|
||||||
) {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableListState(
|
||||||
|
flowProvider = { Database.songs(sortBy, sortOrder) },
|
||||||
|
stateSaver = DetailedSongListSaver,
|
||||||
|
key1 = sortBy,
|
||||||
|
key2 = sortOrder
|
||||||
|
)
|
||||||
|
|
||||||
|
// var items by rememberSaveable(stateSaver = DetailedSongListSaver) {
|
||||||
|
// mutableStateOf(emptyList())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var hasToRecollect by rememberSaveable(sortBy, sortOrder) {
|
||||||
|
// println("hasToRecollect: $sortBy, $sortOrder")
|
||||||
|
// mutableStateOf(true)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// LaunchedEffect(sortBy, sortOrder) {
|
||||||
|
// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder")
|
||||||
|
// Database.songs(sortBy, sortOrder)
|
||||||
|
// .flowOn(Dispatchers.IO)
|
||||||
|
// .drop(if (hasToRecollect) 0 else 1)
|
||||||
|
// .collect {
|
||||||
|
// hasToRecollect = false
|
||||||
|
// println("[${System.currentTimeMillis()}] collecting... ")
|
||||||
|
// items = it
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
val sortOrderIconRotation by animateFloatAsState(
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,6 +110,8 @@ fun HomeSongList(
|
|||||||
.background(colorPalette.background0)
|
.background(colorPalette.background0)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
// println("[${System.currentTimeMillis()}] LazyColumn")
|
||||||
|
|
||||||
item(
|
item(
|
||||||
key = "header",
|
key = "header",
|
||||||
contentType = 0
|
contentType = 0
|
||||||
@@ -82,14 +120,14 @@ fun HomeSongList(
|
|||||||
@Composable
|
@Composable
|
||||||
fun Item(
|
fun Item(
|
||||||
@DrawableRes iconId: Int,
|
@DrawableRes iconId: Int,
|
||||||
sortBy: SongSortBy
|
targetSortBy: SongSortBy
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(iconId),
|
painter = painterResource(iconId),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortBy = sortBy }
|
.clickable { sortBy = targetSortBy }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
)
|
)
|
||||||
@@ -97,17 +135,17 @@ fun HomeSongList(
|
|||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.trending,
|
iconId = R.drawable.trending,
|
||||||
sortBy = SongSortBy.PlayTime
|
targetSortBy = SongSortBy.PlayTime
|
||||||
)
|
)
|
||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.text,
|
iconId = R.drawable.text,
|
||||||
sortBy = SongSortBy.Title
|
targetSortBy = SongSortBy.Title
|
||||||
)
|
)
|
||||||
|
|
||||||
Item(
|
Item(
|
||||||
iconId = R.drawable.time,
|
iconId = R.drawable.time,
|
||||||
sortBy = SongSortBy.DateAdded
|
targetSortBy = SongSortBy.DateAdded
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
@@ -120,7 +158,7 @@ fun HomeSongList(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
.clickable { sortOrder = !sortOrder }
|
||||||
.padding(all = 4.dp)
|
.padding(all = 4.dp)
|
||||||
.size(18.dp)
|
.size(18.dp)
|
||||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
@@ -129,25 +167,24 @@ fun HomeSongList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = viewModel.items,
|
items = items,
|
||||||
key = { _, song -> song.id }
|
key = { _, song -> song.id }
|
||||||
) { index, song ->
|
) { index, song ->
|
||||||
SongItem(
|
SongItem(
|
||||||
song = song,
|
song = song,
|
||||||
thumbnailSize = thumbnailSize,
|
thumbnailSize = thumbnailSize,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
items.map(DetailedSong::asMediaItem)?.let { mediaItems ->
|
||||||
binder?.stopRadio()
|
binder?.stopRadio()
|
||||||
binder?.player?.forcePlayAtIndex(
|
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
||||||
viewModel.items.map(DetailedSong::asMediaItem),
|
}
|
||||||
index
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
menuContent = {
|
menuContent = {
|
||||||
InHistoryMediaItemMenu(song = song)
|
InHistoryMediaItemMenu(song = song)
|
||||||
},
|
},
|
||||||
onThumbnailContent = {
|
onThumbnailContent = {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = viewModel.sortBy == SongSortBy.PlayTime,
|
visible = sortBy == SongSortBy.PlayTime,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.home
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
|
||||||
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
|
||||||
import it.vfsfitvnm.vimusic.utils.putEnum
|
|
||||||
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class HomeSongListViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
var items by mutableStateOf(emptyList<DetailedSong>())
|
|
||||||
private set
|
|
||||||
|
|
||||||
var sortBy by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
songSortByKey,
|
|
||||||
SongSortBy.DateAdded
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(songSortByKey, it) }
|
|
||||||
collectItems(sortBy = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sortOrder by mutableStatePreferenceOf(
|
|
||||||
preferences.getEnum(
|
|
||||||
songSortOrderKey,
|
|
||||||
SortOrder.Ascending
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
preferences.edit { putEnum(songSortOrderKey, it) }
|
|
||||||
collectItems(sortOrder = it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences
|
|
||||||
get() = getApplication<Application>().preferences
|
|
||||||
|
|
||||||
init {
|
|
||||||
collectItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun collectItems(sortBy: SongSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
|
|
||||||
job?.cancel()
|
|
||||||
job = viewModelScope.launch {
|
|
||||||
Database.songs(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
|
||||||
items = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.text.BasicText
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -25,12 +26,11 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
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.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
@@ -41,6 +41,7 @@ import it.vfsfitvnm.vimusic.utils.align
|
|||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableListState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
@@ -49,19 +50,19 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|||||||
@Composable
|
@Composable
|
||||||
fun LocalSongSearch(
|
fun LocalSongSearch(
|
||||||
textFieldValue: TextFieldValue,
|
textFieldValue: TextFieldValue,
|
||||||
onTextFieldValueChanged: (TextFieldValue) -> Unit,
|
onTextFieldValueChanged: (TextFieldValue) -> Unit
|
||||||
viewModel: LocalSongSearchViewModel = viewModel(
|
|
||||||
key = textFieldValue.text,
|
|
||||||
factory = object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return LocalSongSearchViewModel(textFieldValue.text) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
|
val items by produceSaveableListState(
|
||||||
|
flowProvider = {
|
||||||
|
Database.search("%${textFieldValue.text}%")
|
||||||
|
},
|
||||||
|
stateSaver = DetailedSongListSaver,
|
||||||
|
key1 = textFieldValue.text
|
||||||
|
)
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -122,7 +123,7 @@ fun LocalSongSearch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = viewModel.items,
|
items = items,
|
||||||
key = DetailedSong::id,
|
key = DetailedSong::id,
|
||||||
) { song ->
|
) { song ->
|
||||||
SongItem(
|
SongItem(
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.search
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class LocalSongSearchViewModel(text: String) : ViewModel() {
|
|
||||||
var items by mutableStateOf(emptyList<DetailedSong>())
|
|
||||||
private set
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (text.isNotEmpty()) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
Database.search("%$text%").collect {
|
|
||||||
items = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
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
|
||||||
@@ -39,21 +40,24 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.models.SearchQuery
|
import it.vfsfitvnm.vimusic.models.SearchQuery
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.StringListResultSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.utils.align
|
import it.vfsfitvnm.vimusic.utils.align
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableListState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnlineSearch(
|
fun OnlineSearch(
|
||||||
@@ -61,19 +65,30 @@ fun OnlineSearch(
|
|||||||
onTextFieldValueChanged: (TextFieldValue) -> Unit,
|
onTextFieldValueChanged: (TextFieldValue) -> Unit,
|
||||||
isOpenableUrl: Boolean,
|
isOpenableUrl: Boolean,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
onUri: () -> Unit,
|
onUri: () -> Unit
|
||||||
viewModel: OnlineSearchViewModel = viewModel(
|
|
||||||
key = textFieldValue.text,
|
|
||||||
factory = object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return OnlineSearchViewModel(textFieldValue.text) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
val history by produceSaveableListState(
|
||||||
|
flowProvider = {
|
||||||
|
Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new ->
|
||||||
|
old.size == new.size
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stateSaver = SearchQueryListSaver,
|
||||||
|
key1 = textFieldValue.text
|
||||||
|
)
|
||||||
|
|
||||||
|
val suggestionsResult by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = StringListResultSaver,
|
||||||
|
key1 = textFieldValue.text
|
||||||
|
) {
|
||||||
|
if (textFieldValue.text.isNotEmpty()) {
|
||||||
|
value = YouTube.getSearchSuggestions(textFieldValue.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val timeIconPainter = painterResource(R.drawable.time)
|
val timeIconPainter = painterResource(R.drawable.time)
|
||||||
val closeIconPainter = painterResource(R.drawable.close)
|
val closeIconPainter = painterResource(R.drawable.close)
|
||||||
val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward)
|
val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward)
|
||||||
@@ -173,7 +188,7 @@ fun OnlineSearch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = viewModel.history,
|
items = history,
|
||||||
key = SearchQuery::id
|
key = SearchQuery::id
|
||||||
) { searchQuery ->
|
) { searchQuery ->
|
||||||
Row(
|
Row(
|
||||||
@@ -241,7 +256,7 @@ fun OnlineSearch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.suggestionsResult?.getOrNull()?.let { suggestions ->
|
suggestionsResult?.getOrNull()?.let { suggestions ->
|
||||||
items(items = suggestions) { suggestion ->
|
items(items = suggestions) { suggestion ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -288,7 +303,7 @@ fun OnlineSearch(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: viewModel.suggestionsResult?.exceptionOrNull()?.let { throwable ->
|
} ?: suggestionsResult?.exceptionOrNull()?.let { throwable ->
|
||||||
item {
|
item {
|
||||||
LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {}
|
LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.search
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.models.SearchQuery
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class OnlineSearchViewModel(text: String) : ViewModel() {
|
|
||||||
var history by mutableStateOf(emptyList<SearchQuery>())
|
|
||||||
private set
|
|
||||||
|
|
||||||
var suggestionsResult by mutableStateOf<Result<List<String>?>?>(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
Database.queries("%$text%").distinctUntilChanged { old, new ->
|
|
||||||
old.size == new.size
|
|
||||||
}.collect {
|
|
||||||
history = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.isNotEmpty()) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
suggestionsResult = YouTube.getSearchSuggestions(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,36 +9,59 @@ import androidx.compose.foundation.lazy.LazyItemScope
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.savers.ListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.StringResultSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError
|
import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableState
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <I : YouTube.Item> ItemSearchResult(
|
inline fun <T : YouTube.Item> SearchResult(
|
||||||
query: String,
|
query: String,
|
||||||
filter: String,
|
filter: String,
|
||||||
|
stateSaver: ListSaver<T, List<Any?>>,
|
||||||
crossinline onSearchAgain: () -> Unit,
|
crossinline onSearchAgain: () -> Unit,
|
||||||
viewModel: SearchResultViewModel<I> = viewModel(
|
crossinline itemContent: @Composable LazyItemScope.(T) -> Unit,
|
||||||
key = query + filter,
|
|
||||||
factory = object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return SearchResultViewModel<I>(query, filter) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
crossinline itemContent: @Composable LazyItemScope.(I) -> Unit,
|
|
||||||
noinline itemShimmer: @Composable BoxScope.() -> Unit,
|
noinline itemShimmer: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var items by rememberSaveable(query, filter, stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(listOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
val (continuationResultState, fetch) = produceSaveableRelaunchableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = StringResultSaver,
|
||||||
|
key1 = query,
|
||||||
|
key2 = filter
|
||||||
|
) {
|
||||||
|
val token = value?.getOrNull()
|
||||||
|
|
||||||
|
value = null
|
||||||
|
|
||||||
|
value = withContext(Dispatchers.IO) {
|
||||||
|
YouTube.search(query, filter, token)
|
||||||
|
}?.map { searchResult ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
items = items.plus(searchResult.items as List<T>).distinctBy(YouTube.Item::key)
|
||||||
|
searchResult.continuation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val continuationResult by continuationResultState
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -60,27 +83,27 @@ inline fun <I : YouTube.Item> ItemSearchResult(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items(
|
items(
|
||||||
items = viewModel.items,
|
items = items,
|
||||||
key = { it.key!! },
|
key = { it.key!! },
|
||||||
itemContent = itemContent
|
itemContent = itemContent
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.continuationResult?.getOrNull()?.let {
|
continuationResult?.getOrNull()?.let {
|
||||||
if (viewModel.items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
SideEffect(viewModel::fetch)
|
SideEffect(fetch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: viewModel.continuationResult?.exceptionOrNull()?.let { throwable ->
|
} ?: continuationResult?.exceptionOrNull()?.let { throwable ->
|
||||||
item {
|
item {
|
||||||
SearchResultLoadingOrError(
|
SearchResultLoadingOrError(
|
||||||
errorMessage = throwable.javaClass.canonicalName,
|
errorMessage = throwable.javaClass.canonicalName,
|
||||||
onRetry = viewModel::fetch,
|
onRetry = fetch,
|
||||||
shimmerContent = {}
|
shimmerContent = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} ?: viewModel.continuationResult?.let {
|
} ?: continuationResult?.let {
|
||||||
if (viewModel.items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
TextCard(icon = R.drawable.sad) {
|
TextCard(icon = R.drawable.sad) {
|
||||||
Title(text = "No results found")
|
Title(text = "No results found")
|
||||||
@@ -90,7 +113,7 @@ inline fun <I : YouTube.Item> ItemSearchResult(
|
|||||||
}
|
}
|
||||||
} ?: item(key = "loading") {
|
} ?: item(key = "loading") {
|
||||||
SearchResultLoadingOrError(
|
SearchResultLoadingOrError(
|
||||||
itemCount = if (viewModel.items.isEmpty()) 8 else 3,
|
itemCount = if (items.isEmpty()) 8 else 3,
|
||||||
shimmerContent = itemShimmer
|
shimmerContent = itemShimmer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import androidx.compose.ui.unit.dp
|
|||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeArtistListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.PlaylistScreen
|
import it.vfsfitvnm.vimusic.ui.screens.PlaylistScreen
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
@@ -85,10 +90,11 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ItemSearchResult<YouTube.Item.Song>(
|
SearchResult<YouTube.Item.Song>(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
|
stateSaver = YouTubeSongListSaver,
|
||||||
itemContent = { song ->
|
itemContent = { song ->
|
||||||
SmallSongItem(
|
SmallSongItem(
|
||||||
song = song,
|
song = song,
|
||||||
@@ -110,9 +116,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ItemSearchResult<YouTube.Item.Album>(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
|
stateSaver = YouTubeAlbumListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
itemContent = { album ->
|
itemContent = { album ->
|
||||||
AlbumItem(
|
AlbumItem(
|
||||||
@@ -138,9 +145,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = 64.dp
|
val thumbnailSizeDp = 64.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ItemSearchResult<YouTube.Item.Artist>(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
|
stateSaver = YouTubeArtistListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
itemContent = { artist ->
|
itemContent = { artist ->
|
||||||
ArtistItem(
|
ArtistItem(
|
||||||
@@ -165,9 +173,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailHeightDp = 72.dp
|
val thumbnailHeightDp = 72.dp
|
||||||
val thumbnailWidthDp = 128.dp
|
val thumbnailWidthDp = 128.dp
|
||||||
|
|
||||||
ItemSearchResult<YouTube.Item.Video>(
|
SearchResult<YouTube.Item.Video>(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
|
stateSaver = YouTubeVideoListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
itemContent = { video ->
|
itemContent = { video ->
|
||||||
VideoItem(
|
VideoItem(
|
||||||
@@ -194,9 +203,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ItemSearchResult<YouTube.Item.Playlist>(
|
SearchResult<YouTube.Item.Playlist>(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
|
stateSaver = YouTubePlaylistListSaver,
|
||||||
onSearchAgain = onSearchAgain,
|
onSearchAgain = onSearchAgain,
|
||||||
itemContent = { playlist ->
|
itemContent = { playlist ->
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens.searchresult
|
|
||||||
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class SearchResultViewModel<T : YouTube.Item>(
|
|
||||||
private val query: String,
|
|
||||||
private val filter: String
|
|
||||||
) : ViewModel() {
|
|
||||||
var items by mutableStateOf(listOf<T>())
|
|
||||||
|
|
||||||
var continuationResult by mutableStateOf<Result<String?>?>(null)
|
|
||||||
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
fetch()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetch() {
|
|
||||||
job?.cancel()
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
val token = continuationResult?.getOrNull()
|
|
||||||
|
|
||||||
continuationResult = null
|
|
||||||
|
|
||||||
continuationResult = withContext(Dispatchers.IO) {
|
|
||||||
YouTube.search(query, filter, token)
|
|
||||||
}?.map { searchResult ->
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
items = items.plus(searchResult.items as List<T>).distinctBy(YouTube.Item::key)
|
|
||||||
searchResult.continuation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -83,7 +83,7 @@ fun SmallSongItem(
|
|||||||
SongItem(
|
SongItem(
|
||||||
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
|
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
|
||||||
title = song.info.name,
|
title = song.info.name,
|
||||||
authors = song.authors.joinToString("") { it.name },
|
authors = song.authors?.joinToString("") { it.name } ?: "",
|
||||||
durationText = song.durationText,
|
durationText = song.durationText,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
menuContent = {
|
menuContent = {
|
||||||
@@ -158,13 +158,13 @@ fun VideoItem(
|
|||||||
)
|
)
|
||||||
|
|
||||||
BasicText(
|
BasicText(
|
||||||
text = video.authors.joinToString("") { it.name },
|
text = video.authors?.joinToString("") { it.name } ?: "",
|
||||||
style = typography.xs.semiBold.secondary,
|
style = typography.xs.semiBold.secondary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|
||||||
video.views.firstOrNull()?.name?.let { viewsText ->
|
video.viewsText?.let { viewsText ->
|
||||||
BasicText(
|
BasicText(
|
||||||
text = viewsText,
|
text = viewsText,
|
||||||
style = typography.xxs.medium.secondary,
|
style = typography.xxs.medium.secondary,
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import it.vfsfitvnm.vimusic.savers.ListSaver
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableListState(
|
||||||
|
flowProvider: () -> Flow<List<T>>,
|
||||||
|
stateSaver: ListSaver<T, List<Any?>>,
|
||||||
|
): State<List<T>> {
|
||||||
|
val state = rememberSaveable(stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasToRecollect by rememberSaveable {
|
||||||
|
mutableStateOf(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
flowProvider()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.drop(if (hasToRecollect) 0 else 1)
|
||||||
|
.collect {
|
||||||
|
hasToRecollect = false
|
||||||
|
state.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableListState(
|
||||||
|
flowProvider: () -> Flow<List<T>>,
|
||||||
|
stateSaver: ListSaver<T, List<Any?>>,
|
||||||
|
key1: Any?,
|
||||||
|
): State<List<T>> {
|
||||||
|
val state = rememberSaveable(stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasToRecollect by rememberSaveable(key1) {
|
||||||
|
// println("hasToRecollect: $sortBy, $sortOrder")
|
||||||
|
mutableStateOf(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1) {
|
||||||
|
// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder")
|
||||||
|
flowProvider()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.drop(if (hasToRecollect) 0 else 1)
|
||||||
|
.collect {
|
||||||
|
hasToRecollect = false
|
||||||
|
// println("[${System.currentTimeMillis()}] collecting... ")
|
||||||
|
state.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableListState(
|
||||||
|
flowProvider: () -> Flow<List<T>>,
|
||||||
|
stateSaver: ListSaver<T, List<Any?>>,
|
||||||
|
key1: Any?,
|
||||||
|
key2: Any?,
|
||||||
|
): State<List<T>> {
|
||||||
|
val state = rememberSaveable(stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
// var hasToRecollect by rememberSaveable(key1, key2) {
|
||||||
|
//// println("hasToRecollect: $sortBy, $sortOrder")
|
||||||
|
// mutableStateOf(true)
|
||||||
|
// }
|
||||||
|
|
||||||
|
LaunchedEffect(key1, key2) {
|
||||||
|
// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder")
|
||||||
|
flowProvider()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
// .drop(if (hasToRecollect) 0 else 1)
|
||||||
|
.collect {
|
||||||
|
// hasToRecollect = false
|
||||||
|
// println("[${System.currentTimeMillis()}] collecting... ")
|
||||||
|
state.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.ProduceStateScope
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.experimental.ExperimentalTypeInference
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTypeInference::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableState(
|
||||||
|
initialValue: T,
|
||||||
|
stateSaver: Saver<T, out Any>,
|
||||||
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
|
): State<T> {
|
||||||
|
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
var hasToFetch by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (hasToFetch) {
|
||||||
|
ProduceSaveableStateScope(result, coroutineContext).producer()
|
||||||
|
hasToFetch = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTypeInference::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableState(
|
||||||
|
initialValue: T,
|
||||||
|
stateSaver: Saver<T, out Any>,
|
||||||
|
key1: Any?,
|
||||||
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
|
): State<T> {
|
||||||
|
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
var hasToFetch by rememberSaveable(key1) { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(key1) {
|
||||||
|
if (hasToFetch) {
|
||||||
|
ProduceSaveableStateScope(result, coroutineContext).producer()
|
||||||
|
hasToFetch = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTypeInference::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableState(
|
||||||
|
initialValue: T,
|
||||||
|
stateSaver: Saver<T, out Any>,
|
||||||
|
key1: Any?,
|
||||||
|
key2: Any?,
|
||||||
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
|
): State<T> {
|
||||||
|
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (hasToFetch) {
|
||||||
|
ProduceSaveableStateScope(result, coroutineContext).producer()
|
||||||
|
hasToFetch = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTypeInference::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableRelaunchableState(
|
||||||
|
initialValue: T,
|
||||||
|
stateSaver: Saver<T, out Any>,
|
||||||
|
key1: Any?,
|
||||||
|
key2: Any?,
|
||||||
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
|
): Pair<State<T>, () -> Unit> {
|
||||||
|
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) }
|
||||||
|
|
||||||
|
val relaunchableEffect = relaunchableEffect(key1, key2) {
|
||||||
|
if (hasToFetch) {
|
||||||
|
ProduceSaveableStateScope(result, coroutineContext).producer()
|
||||||
|
hasToFetch = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result to {
|
||||||
|
hasToFetch = true
|
||||||
|
relaunchableEffect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProduceSaveableStateScope<T>(
|
||||||
|
state: MutableState<T>,
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
) : ProduceStateScope<T>, MutableState<T> by state {
|
||||||
|
override suspend fun awaitDispose(onDispose: () -> Unit): Nothing {
|
||||||
|
try {
|
||||||
|
suspendCancellableCoroutine<Nothing> { }
|
||||||
|
} finally {
|
||||||
|
onDispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem
|
|||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setTitle(info.name)
|
.setTitle(info.name)
|
||||||
.setArtist(authors.joinToString("") { it.name })
|
.setArtist(authors?.joinToString("") { it.name })
|
||||||
.setAlbumTitle(album?.name)
|
.setAlbumTitle(album?.name)
|
||||||
.setArtworkUri(thumbnail?.url?.toUri())
|
.setArtworkUri(thumbnail?.url?.toUri())
|
||||||
.setExtras(
|
.setExtras(
|
||||||
@@ -36,8 +36,8 @@ val YouTube.Item.Song.asMediaItem: MediaItem
|
|||||||
"videoId" to info.endpoint!!.videoId,
|
"videoId" to info.endpoint!!.videoId,
|
||||||
"albumId" to album?.endpoint?.browseId,
|
"albumId" to album?.endpoint?.browseId,
|
||||||
"durationText" to durationText,
|
"durationText" to durationText,
|
||||||
"artistNames" to authors.filter { it.endpoint != null }.map { it.name },
|
"artistNames" to authors?.filter { it.endpoint != null }?.map { it.name },
|
||||||
"artistIds" to authors.mapNotNull { it.endpoint?.browseId },
|
"artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
@@ -52,14 +52,14 @@ val YouTube.Item.Video.asMediaItem: MediaItem
|
|||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setTitle(info.name)
|
.setTitle(info.name)
|
||||||
.setArtist(authors.joinToString("") { it.name })
|
.setArtist(authors?.joinToString("") { it.name })
|
||||||
.setArtworkUri(thumbnail?.url?.toUri())
|
.setArtworkUri(thumbnail?.url?.toUri())
|
||||||
.setExtras(
|
.setExtras(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
"videoId" to info.endpoint!!.videoId,
|
"videoId" to info.endpoint!!.videoId,
|
||||||
"durationText" to durationText,
|
"durationText" to durationText,
|
||||||
"artistNames" to if (isOfficialMusicVideo) authors.filter { it.endpoint != null }.map { it.name } else null,
|
"artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.map { it.name } else null,
|
||||||
"artistIds" to if (isOfficialMusicVideo) authors.mapNotNull { it.endpoint?.browseId } else null,
|
"artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
library("compose-shimmer", "com.valentinilk.shimmer", "compose-shimmer").version("1.0.3")
|
library("compose-shimmer", "com.valentinilk.shimmer", "compose-shimmer").version("1.0.3")
|
||||||
|
|
||||||
library("compose-viewmodel", "androidx.lifecycle", "lifecycle-viewmodel-compose").version("2.6.0-alpha02")
|
|
||||||
library("compose-activity", "androidx.activity", "activity-compose").version("1.5.1")
|
library("compose-activity", "androidx.activity", "activity-compose").version("1.5.1")
|
||||||
|
|
||||||
library("compose-coil", "io.coil-kt", "coil-compose").version("2.2.1")
|
library("compose-coil", "io.coil-kt", "coil-compose").version("2.2.1")
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ object YouTube {
|
|||||||
|
|
||||||
data class Song(
|
data class Song(
|
||||||
val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
||||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>,
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
|
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||||
val durationText: String?,
|
val durationText: String?,
|
||||||
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||||
@@ -231,8 +231,8 @@ object YouTube {
|
|||||||
|
|
||||||
data class Video(
|
data class Video(
|
||||||
val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
||||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>,
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
val views: List<Info<NavigationEndpoint.Endpoint.Browse>>,
|
val viewsText: String?,
|
||||||
val durationText: String?,
|
val durationText: String?,
|
||||||
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||||
) : Item() {
|
) : Item() {
|
||||||
@@ -263,14 +263,14 @@ object YouTube {
|
|||||||
info = Info.from(mainRuns.first()),
|
info = Info.from(mainRuns.first()),
|
||||||
authors = otherRuns
|
authors = otherRuns
|
||||||
.getOrNull(otherRuns.lastIndex - 2)
|
.getOrNull(otherRuns.lastIndex - 2)
|
||||||
?.map(Info.Companion::from)
|
?.map(Info.Companion::from),
|
||||||
?: emptyList(),
|
viewsText = otherRuns
|
||||||
views = otherRuns
|
|
||||||
.getOrNull(otherRuns.lastIndex - 1)
|
.getOrNull(otherRuns.lastIndex - 1)
|
||||||
?.map(Info.Companion::from) ?: emptyList(),
|
?.firstOrNull()
|
||||||
|
?.text,
|
||||||
durationText = otherRuns
|
durationText = otherRuns
|
||||||
.getOrNull(otherRuns.lastIndex)
|
.getOrNull(otherRuns.lastIndex)
|
||||||
?.first()
|
?.firstOrNull()
|
||||||
?.text,
|
?.text,
|
||||||
thumbnail = content
|
thumbnail = content
|
||||||
.thumbnail
|
.thumbnail
|
||||||
|
|||||||
Reference in New Issue
Block a user