Drop ViewModel

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

View File

@@ -84,7 +84,6 @@ dependencies {
implementation(libs.compose.ripple) implementation(libs.compose.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)

View File

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

View File

@@ -1,608 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 19,
"identityHash": "41479c8284963d3533c4baa46d7464a6",
"entities": [
{
"tableName": "Song",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistsText",
"columnName": "artistsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durationText",
"columnName": "durationText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lyrics",
"columnName": "lyrics",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "synchronizedLyrics",
"columnName": "synchronizedLyrics",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "likedAt",
"columnName": "likedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "totalPlayTimeMs",
"columnName": "totalPlayTimeMs",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongPlaylistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"playlistId"
]
},
"indices": [
{
"name": "index_SongPlaylistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongPlaylistMap_playlistId",
"unique": false,
"columnNames": [
"playlistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Playlist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"playlistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Playlist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "browseId",
"columnName": "browseId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Artist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shuffleVideoId",
"columnName": "shuffleVideoId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shufflePlaylistId",
"columnName": "shufflePlaylistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "radioVideoId",
"columnName": "radioVideoId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "radioPlaylistId",
"columnName": "radioPlaylistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongArtistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"artistId"
]
},
"indices": [
{
"name": "index_SongArtistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongArtistMap_artistId",
"unique": false,
"columnNames": [
"artistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Artist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"artistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Album",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "authorsText",
"columnName": "authorsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareUrl",
"columnName": "shareUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarkedAt",
"columnName": "bookmarkedAt",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongAlbumMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"albumId"
]
},
"indices": [
{
"name": "index_SongAlbumMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongAlbumMap_albumId",
"unique": false,
"columnNames": [
"albumId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Album",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"albumId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "SearchQuery",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_SearchQuery_query",
"unique": true,
"columnNames": [
"query"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
}
],
"foreignKeys": []
},
{
"tableName": "QueuedMediaItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaItem",
"columnName": "mediaItem",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Format",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itag",
"columnName": "itag",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentLength",
"columnName": "contentLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "lastModified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "loudnessDb",
"columnName": "loudnessDb",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [
{
"viewName": "SortedSongPlaylistMap",
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
},
{
"viewName": "SortedSongAlbumMap",
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position"
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '41479c8284963d3533c4baa46d7464a6')"
]
}
}

View File

@@ -1,614 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 20,
"identityHash": "821aa30ff7d14b31e839b2f3b2312f78",
"entities": [
{
"tableName": "Song",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistsText",
"columnName": "artistsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durationText",
"columnName": "durationText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lyrics",
"columnName": "lyrics",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "synchronizedLyrics",
"columnName": "synchronizedLyrics",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "likedAt",
"columnName": "likedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "totalPlayTimeMs",
"columnName": "totalPlayTimeMs",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongPlaylistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"playlistId"
]
},
"indices": [
{
"name": "index_SongPlaylistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongPlaylistMap_playlistId",
"unique": false,
"columnNames": [
"playlistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Playlist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"playlistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Playlist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "browseId",
"columnName": "browseId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Artist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shuffleVideoId",
"columnName": "shuffleVideoId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shufflePlaylistId",
"columnName": "shufflePlaylistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "radioVideoId",
"columnName": "radioVideoId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "radioPlaylistId",
"columnName": "radioPlaylistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarkedAt",
"columnName": "bookmarkedAt",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongArtistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"artistId"
]
},
"indices": [
{
"name": "index_SongArtistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongArtistMap_artistId",
"unique": false,
"columnNames": [
"artistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Artist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"artistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Album",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "authorsText",
"columnName": "authorsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareUrl",
"columnName": "shareUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarkedAt",
"columnName": "bookmarkedAt",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongAlbumMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"albumId"
]
},
"indices": [
{
"name": "index_SongAlbumMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongAlbumMap_albumId",
"unique": false,
"columnNames": [
"albumId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Album",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"albumId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "SearchQuery",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_SearchQuery_query",
"unique": true,
"columnNames": [
"query"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
}
],
"foreignKeys": []
},
{
"tableName": "QueuedMediaItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaItem",
"columnName": "mediaItem",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Format",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itag",
"columnName": "itag",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mimeType",
"columnName": "mimeType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentLength",
"columnName": "contentLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastModified",
"columnName": "lastModified",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "loudnessDb",
"columnName": "loudnessDb",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"views": [
{
"viewName": "SortedSongPlaylistMap",
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
},
{
"viewName": "SortedSongAlbumMap",
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position"
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '821aa30ff7d14b31e839b2f3b2312f78')"
]
}
}

View File

@@ -34,7 +34,6 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.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)

View File

@@ -1,22 +0,0 @@
package it.vfsfitvnm.vimusic.models
import androidx.compose.runtime.Immutable
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
@Immutable
data class AlbumWithSongs(
@Embedded val album: Album,
@Relation(
entity = Song::class,
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = SortedSongAlbumMap::class,
parentColumn = "albumId",
entityColumn = "songId"
)
)
val songs: List<DetailedSong>
)

View File

@@ -1,13 +0,0 @@
package it.vfsfitvnm.vimusic.models
import androidx.compose.runtime.Immutable
import androidx.room.ColumnInfo
import androidx.room.DatabaseView
@Immutable
@DatabaseView("SELECT * FROM SongAlbumMap ORDER BY position")
data class SortedSongAlbumMap(
@ColumnInfo(index = true) val songId: String,
@ColumnInfo(index = true) val albumId: String,
val position: Int
)

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.Album
object AlbumSaver : Saver<Album, List<Any?>> {
override fun SaverScope.save(value: Album): List<Any?> = listOf(
value.id,
value.title,
value.thumbnailUrl,
value.year,
value.authorsText,
value.shareUrl,
value.timestamp,
value.bookmarkedAt,
)
override fun restore(value: List<Any?>): Album = Album(
id = value[0] as String,
title = value[1] as String,
thumbnailUrl = value[2] as String?,
year = value[3] as String?,
authorsText = value[4] as String?,
shareUrl = value[5] as String?,
timestamp = value[6] as Long?,
bookmarkedAt = value[7] as Long?,
)
}

View File

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

View File

@@ -0,0 +1,34 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.models.Playlist
object ArtistSaver : Saver<Artist, List<Any?>> {
override fun SaverScope.save(value: Artist): List<Any?> = listOf(
value.id,
value.name,
value.thumbnailUrl,
value.info,
value.shuffleVideoId,
value.shufflePlaylistId,
value.radioVideoId,
value.radioPlaylistId,
value.timestamp,
value.bookmarkedAt,
)
override fun restore(value: List<Any?>): Artist = Artist(
id = value[0] as String,
name = value[1] as String,
thumbnailUrl = value[2] as String?,
info = value[3] as String?,
shuffleVideoId = value[4] as String?,
shufflePlaylistId = value[5] as String?,
radioVideoId = value[6] as String?,
radioPlaylistId = value[7] as String?,
timestamp = value[8] as Long?,
bookmarkedAt = value[9] as Long?,
)
}

View File

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

View File

@@ -0,0 +1,33 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.DetailedSong
object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
override fun SaverScope.save(value: DetailedSong): List<Any?> =
listOf(
value.id,
value.title,
value.artistsText,
value.durationText,
value.thumbnailUrl,
value.totalPlayTimeMs,
value.albumId,
value.artists?.let { with(InfoListSaver) { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>): DetailedSong? {
return if (value.size == 8) DetailedSong(
id = value[0] as String,
title = value[1] as String,
artistsText = value[2] as String?,
durationText = value[3] as String,
thumbnailUrl = value[4] as String?,
totalPlayTimeMs = value[5] as Long,
albumId = value[6] as String?,
artists = InfoListSaver.restore(value[7] as List<List<String>>)
) else null
}
}

View File

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

View File

@@ -0,0 +1,16 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.Info
object InfoSaver : Saver<Info, List<String>> {
override fun SaverScope.save(value: Info): List<String> = listOf(value.id, value.name)
override fun restore(value: List<String>): Info? {
return if (value.size == 2) Info(
id = value[0],
name = value[1],
) else null
}
}

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.PlaylistPreview
object PlaylistPreviewSaver : Saver<PlaylistPreview, List<Any?>> {
override fun SaverScope.save(value: PlaylistPreview): List<Any> {
return listOf(
with(PlaylistSaver) { save(value.playlist) },
value.songCount,
)
}
override fun restore(value: List<Any?>): PlaylistPreview? {
return if (value.size == 2) PlaylistPreview(
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
songCount = value[1] as Int,
) else null
}
}

View File

@@ -0,0 +1,19 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.Playlist
object PlaylistSaver : Saver<Playlist, List<Any?>> {
override fun SaverScope.save(value: Playlist): List<Any?> = listOf(
value.id,
value.name,
value.browseId,
)
override fun restore(value: List<Any?>): Playlist = Playlist(
id = value[0] as Long,
name = value[1] as String,
browseId = value[2] as String?,
)
}

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.vimusic.models.SearchQuery
object SearchQuerySaver : Saver<SearchQuery, List<Any?>> {
override fun SaverScope.save(value: SearchQuery): List<Any?> = listOf(
value.id,
value.query,
)
override fun restore(value: List<Any?>) = SearchQuery(
id = value[0] as Long,
query = value[1] as String
)
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeAlbumSaver : Saver<YouTube.Item.Album, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Album): List<Any?> = listOf(
with(YouTubeBrowseInfoSaver) { save(value.info) },
with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } },
value.year,
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Item.Album(
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
year = value[2] as String?,
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View File

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

View File

@@ -0,0 +1,19 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
with(YouTubeBrowseInfoSaver) { save(value.info) },
value.subscribersCountText,
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Item.Artist(
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
subscribersCountText = value[1] as String?,
thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View File

@@ -0,0 +1,18 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeBrowseEndpointSaver : Saver<NavigationEndpoint.Endpoint.Browse, List<Any?>> {
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf(
value.browseId,
value.params
)
override fun restore(value: List<Any?>) = NavigationEndpoint.Endpoint.Browse(
browseId = value[0] as String,
params = value[1] as String?,
browseEndpointContextSupportedConfigs = null
)
}

View File

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

View File

@@ -0,0 +1,18 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeBrowseInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
value.name,
with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Info(
name = value[0] as String,
endpoint = (value[1] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore)
)
}

View File

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

View File

@@ -0,0 +1,21 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubePlaylistSaver : Saver<YouTube.Item.Playlist, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Playlist): List<Any?> = listOf(
with(YouTubeBrowseInfoSaver) { save(value.info) },
with(YouTubeBrowseInfoSaver) { value.channel?.let { save(it) } },
value.songCount,
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Item.Playlist(
info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
channel = (value[1] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
songCount = value[2] as Int?,
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View File

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

View File

@@ -0,0 +1,24 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeSongSaver : Saver<YouTube.Item.Song, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Song): List<Any?> = listOf(
with(YouTubeWatchInfoSaver) { save(value.info) },
with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } },
with(YouTubeBrowseInfoSaver) { value.album?.let { save(it) } },
value.durationText,
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Item.Song(
info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
album = (value[2] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View File

@@ -0,0 +1,19 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
object YouTubeThumbnailSaver : Saver<ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail, List<Any?>> {
override fun SaverScope.save(value: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail) = listOf(
value.url,
value.width,
value.height
)
override fun restore(value: List<Any?>) = ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail(
url = value[0] as String,
width = value[1] as Int,
height = value[2] as Int?,
)
}

View File

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

View File

@@ -0,0 +1,24 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
object YouTubeVideoSaver : Saver<YouTube.Item.Video, List<Any?>> {
override fun SaverScope.save(value: YouTube.Item.Video): List<Any?> = listOf(
with(YouTubeWatchInfoSaver) { save(value.info) },
with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } },
value.viewsText,
value.durationText,
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
)
@Suppress("UNCHECKED_CAST")
override fun restore(value: List<Any?>) = YouTube.Item.Video(
info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
viewsText = value[2] as String?,
durationText = value[3] as String?,
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
)
}

View File

@@ -0,0 +1,24 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeWatchEndpointSaver : Saver<NavigationEndpoint.Endpoint.Watch, List<Any?>> {
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf(
value.params,
value.playlistId,
value.videoId,
value.index,
value.playlistSetVideoId,
)
override fun restore(value: List<Any?>) = NavigationEndpoint.Endpoint.Watch(
params = value[0] as String?,
playlistId = value[1] as String?,
videoId = value[2] as String?,
index = value[3] as Int?,
playlistSetVideoId = value[4] as String?,
watchEndpointMusicSupportedConfigs = null
)
}

View File

@@ -0,0 +1,18 @@
package it.vfsfitvnm.vimusic.savers
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
object YouTubeWatchInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
value.name,
with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } }
)
override fun restore(value: List<Any?>) = YouTube.Info(
name = value[0] as String,
endpoint = (value[1] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore)
)
}

View File

@@ -9,6 +9,7 @@ import it.vfsfitvnm.route.Route1
import it.vfsfitvnm.route.RouteHandlerScope import it.vfsfitvnm.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")

View File

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

View File

@@ -1,66 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.album
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.AlbumWithSongs
import it.vfsfitvnm.vimusic.models.SongAlbumMap
import it.vfsfitvnm.vimusic.utils.toMediaItem
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class AlbumOverviewViewModel(browseId: String) : ViewModel() {
var result by mutableStateOf<Result<AlbumWithSongs?>?>(null)
private set
private var job: Job? = null
init {
fetch(browseId)
}
fun fetch(browseId: String) {
job?.cancel()
result = null
job = viewModelScope.launch(Dispatchers.IO) {
Database.albumWithSongs(browseId).collect { albumWithSongs ->
result = if (albumWithSongs?.album?.timestamp == null) {
YouTube.album(browseId)?.map { youtubeAlbum ->
Database.upsert(
Album(
id = browseId,
title = youtubeAlbum.title,
thumbnailUrl = youtubeAlbum.thumbnail?.url,
year = youtubeAlbum.year,
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
shareUrl = youtubeAlbum.url,
timestamp = System.currentTimeMillis()
),
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
Database.insert(mediaItem)
SongAlbumMap(
songId = mediaItem.mediaId,
albumId = browseId,
position = position
)
}
} ?: emptyList()
)
null
}
} else {
Result.success(albumWithSongs)
}
}
}
}
}

View File

@@ -3,11 +3,21 @@ package it.vfsfitvnm.vimusic.ui.screens.album
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.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,
)
} }
} }
} }

View File

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

View File

@@ -1,66 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.artist
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.AlbumWithSongs
import it.vfsfitvnm.vimusic.models.SongAlbumMap
import it.vfsfitvnm.vimusic.utils.toMediaItem
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class ArtistOverviewViewModel(browseId: String) : ViewModel() {
var result by mutableStateOf<Result<AlbumWithSongs?>?>(null)
private set
private var job: Job? = null
init {
fetch(browseId)
}
fun fetch(browseId: String) {
job?.cancel()
result = null
job = viewModelScope.launch(Dispatchers.IO) {
Database.albumWithSongs(browseId).collect { albumWithSongs ->
result = if (albumWithSongs?.album?.timestamp == null) {
YouTube.album(browseId)?.map { youtubeAlbum ->
Database.upsert(
Album(
id = browseId,
title = youtubeAlbum.title,
thumbnailUrl = youtubeAlbum.thumbnail?.url,
year = youtubeAlbum.year,
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
shareUrl = youtubeAlbum.url,
timestamp = System.currentTimeMillis()
),
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
Database.insert(mediaItem)
SongAlbumMap(
songId = mediaItem.mediaId,
albumId = browseId,
position = position
)
}
} ?: emptyList()
)
null
}
} else {
Result.success(albumWithSongs)
}
}
}
}
}

View File

@@ -80,7 +80,7 @@ import kotlinx.coroutines.runBlocking
@OptIn(ExperimentalFoundationApi::class) @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)

View File

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

View File

@@ -1,67 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.home
import android.app.Application
import android.content.SharedPreferences
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.enums.AlbumSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.utils.albumSortByKey
import it.vfsfitvnm.vimusic.utils.albumSortOrderKey
import it.vfsfitvnm.vimusic.utils.getEnum
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
import it.vfsfitvnm.vimusic.utils.preferences
import it.vfsfitvnm.vimusic.utils.putEnum
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
class HomeAlbumListViewModel(application: Application) : AndroidViewModel(application) {
var items by mutableStateOf(emptyList<Album>())
private set
var sortBy by mutableStatePreferenceOf(
preferences.getEnum(
albumSortByKey,
AlbumSortBy.DateAdded
)
) {
preferences.edit { putEnum(albumSortByKey, it) }
collectItems(sortBy = it)
}
var sortOrder by mutableStatePreferenceOf(
preferences.getEnum(
albumSortOrderKey,
SortOrder.Ascending
)
) {
preferences.edit { putEnum(albumSortOrderKey, it) }
collectItems(sortOrder = it)
}
private var job: Job? = null
private val preferences: SharedPreferences
get() = getApplication<Application>().preferences
init {
collectItems()
}
private fun collectItems(sortBy: AlbumSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
job?.cancel()
job = viewModelScope.launch {
Database.albums(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
items = it
}
}
}
}

View File

@@ -29,6 +29,7 @@ import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.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(

View File

@@ -1,67 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.home
import android.app.Application
import android.content.SharedPreferences
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.enums.ArtistSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.utils.artistSortByKey
import it.vfsfitvnm.vimusic.utils.artistSortOrderKey
import it.vfsfitvnm.vimusic.utils.getEnum
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
import it.vfsfitvnm.vimusic.utils.preferences
import it.vfsfitvnm.vimusic.utils.putEnum
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
class HomeArtistListViewModel(application: Application) : AndroidViewModel(application) {
var items by mutableStateOf(emptyList<Artist>())
private set
var sortBy by mutableStatePreferenceOf(
preferences.getEnum(
artistSortByKey,
ArtistSortBy.DateAdded
)
) {
preferences.edit { putEnum(artistSortByKey, it) }
collectItems(sortBy = it)
}
var sortOrder by mutableStatePreferenceOf(
preferences.getEnum(
artistSortOrderKey,
SortOrder.Ascending
)
) {
preferences.edit { putEnum(artistSortOrderKey, it) }
collectItems(sortOrder = it)
}
private var job: Job? = null
private val preferences: SharedPreferences
get() = getApplication<Application>().preferences
init {
collectItems()
}
private fun collectItems(sortBy: ArtistSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
job?.cancel()
job = viewModelScope.launch {
Database.artists(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
items = it
}
}
}
}

View File

@@ -35,7 +35,6 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.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(

View File

@@ -1,70 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.home
import android.app.Application
import android.content.SharedPreferences
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.utils.getEnum
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
import it.vfsfitvnm.vimusic.utils.preferences
import it.vfsfitvnm.vimusic.utils.putEnum
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
class HomePlaylistListViewModel(application: Application) : AndroidViewModel(application) {
var items by mutableStateOf(emptyList<PlaylistPreview>())
private set
var sortBy by mutableStatePreferenceOf(
preferences.getEnum(
playlistSortByKey,
PlaylistSortBy.DateAdded
)
) {
preferences.edit { putEnum(playlistSortByKey, it) }
collectItems(sortBy = it)
}
var sortOrder by mutableStatePreferenceOf(
preferences.getEnum(
playlistSortOrderKey,
SortOrder.Ascending
)
) {
preferences.edit { putEnum(playlistSortOrderKey, it) }
collectItems(sortOrder = it)
}
private var job: Job? = null
private val preferences: SharedPreferences
get() = getApplication<Application>().preferences
init {
collectItems()
}
private fun collectItems(
sortBy: PlaylistSortBy = this.sortBy,
sortOrder: SortOrder = this.sortOrder
) {
job?.cancel()
job = viewModelScope.launch {
Database.playlistPreviews(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
items = it
}
}
}
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.BasicText import androidx.compose.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

View File

@@ -1,67 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.home
import android.app.Application
import android.content.SharedPreferences
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.utils.getEnum
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
import it.vfsfitvnm.vimusic.utils.preferences
import it.vfsfitvnm.vimusic.utils.putEnum
import it.vfsfitvnm.vimusic.utils.songSortByKey
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
class HomeSongListViewModel(application: Application) : AndroidViewModel(application) {
var items by mutableStateOf(emptyList<DetailedSong>())
private set
var sortBy by mutableStatePreferenceOf(
preferences.getEnum(
songSortByKey,
SongSortBy.DateAdded
)
) {
preferences.edit { putEnum(songSortByKey, it) }
collectItems(sortBy = it)
}
var sortOrder by mutableStatePreferenceOf(
preferences.getEnum(
songSortOrderKey,
SortOrder.Ascending
)
) {
preferences.edit { putEnum(songSortOrderKey, it) }
collectItems(sortOrder = it)
}
private var job: Job? = null
private val preferences: SharedPreferences
get() = getApplication<Application>().preferences
init {
collectItems()
}
private fun collectItems(sortBy: SongSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
job?.cancel()
job = viewModelScope.launch {
Database.songs(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
items = it
}
}
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.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(

View File

@@ -1,25 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.DetailedSong
import kotlinx.coroutines.launch
class LocalSongSearchViewModel(text: String) : ViewModel() {
var items by mutableStateOf(emptyList<DetailedSong>())
private set
init {
if (text.isNotEmpty()) {
viewModelScope.launch {
Database.search("%$text%").collect {
items = it
}
}
}
}
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple import androidx.compose.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) {}
} }

View File

@@ -1,36 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
class OnlineSearchViewModel(text: String) : ViewModel() {
var history by mutableStateOf(emptyList<SearchQuery>())
private set
var suggestionsResult by mutableStateOf<Result<List<String>?>?>(null)
private set
init {
viewModelScope.launch {
Database.queries("%$text%").distinctUntilChanged { old, new ->
old.size == new.size
}.collect {
history = it
}
}
if (text.isNotEmpty()) {
viewModelScope.launch {
suggestionsResult = YouTube.getSearchSuggestions(text)
}
}
}
}

View File

@@ -9,36 +9,59 @@ import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items import androidx.compose.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
) )
} }

View File

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

View File

@@ -1,45 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SearchResultViewModel<T : YouTube.Item>(
private val query: String,
private val filter: String
) : ViewModel() {
var items by mutableStateOf(listOf<T>())
var continuationResult by mutableStateOf<Result<String?>?>(null)
private var job: Job? = null
init {
fetch()
}
fun fetch() {
job?.cancel()
viewModelScope.launch {
val token = continuationResult?.getOrNull()
continuationResult = null
continuationResult = withContext(Dispatchers.IO) {
YouTube.search(query, filter, token)
}?.map { searchResult ->
@Suppress("UNCHECKED_CAST")
items = items.plus(searchResult.items as List<T>).distinctBy(YouTube.Item::key)
searchResult.continuation
}
}
}
}

View File

@@ -83,7 +83,7 @@ fun SmallSongItem(
SongItem( 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,

View File

@@ -0,0 +1,102 @@
package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import it.vfsfitvnm.vimusic.savers.ListSaver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.flowOn
@Composable
fun <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
): State<List<T>> {
val state = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(emptyList())
}
var hasToRecollect by rememberSaveable {
mutableStateOf(true)
}
LaunchedEffect(Unit) {
flowProvider()
.flowOn(Dispatchers.IO)
.drop(if (hasToRecollect) 0 else 1)
.collect {
hasToRecollect = false
state.value = it
}
}
return state
}
@Composable
fun <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
key1: Any?,
): State<List<T>> {
val state = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(emptyList())
}
var hasToRecollect by rememberSaveable(key1) {
// println("hasToRecollect: $sortBy, $sortOrder")
mutableStateOf(true)
}
LaunchedEffect(key1) {
// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder")
flowProvider()
.flowOn(Dispatchers.IO)
.drop(if (hasToRecollect) 0 else 1)
.collect {
hasToRecollect = false
// println("[${System.currentTimeMillis()}] collecting... ")
state.value = it
}
}
return state
}
@Composable
fun <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
key1: Any?,
key2: Any?,
): State<List<T>> {
val state = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(emptyList())
}
// var hasToRecollect by rememberSaveable(key1, key2) {
//// println("hasToRecollect: $sortBy, $sortOrder")
// mutableStateOf(true)
// }
LaunchedEffect(key1, key2) {
// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder")
flowProvider()
.flowOn(Dispatchers.IO)
// .drop(if (hasToRecollect) 0 else 1)
.collect {
// hasToRecollect = false
// println("[${System.currentTimeMillis()}] collecting... ")
state.value = it
}
}
return state
}

View File

@@ -0,0 +1,118 @@
package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ProduceStateScope
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import kotlin.coroutines.CoroutineContext
import kotlin.experimental.ExperimentalTypeInference
import kotlinx.coroutines.suspendCancellableCoroutine
@OptIn(ExperimentalTypeInference::class)
@Composable
fun <T> produceSaveableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
var hasToFetch by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(Unit) {
if (hasToFetch) {
ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false
}
}
return result
}
@OptIn(ExperimentalTypeInference::class)
@Composable
fun <T> produceSaveableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
var hasToFetch by rememberSaveable(key1) { mutableStateOf(true) }
LaunchedEffect(key1) {
if (hasToFetch) {
ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false
}
}
return result
}
@OptIn(ExperimentalTypeInference::class)
@Composable
fun <T> produceSaveableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) }
LaunchedEffect(Unit) {
if (hasToFetch) {
ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false
}
}
return result
}
@OptIn(ExperimentalTypeInference::class)
@Composable
fun <T> produceSaveableRelaunchableState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): Pair<State<T>, () -> Unit> {
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) }
var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) }
val relaunchableEffect = relaunchableEffect(key1, key2) {
if (hasToFetch) {
ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false
}
}
return result to {
hasToFetch = true
relaunchableEffect()
}
}
private class ProduceSaveableStateScope<T>(
state: MutableState<T>,
override val coroutineContext: CoroutineContext
) : ProduceStateScope<T>, MutableState<T> by state {
override suspend fun awaitDispose(onDispose: () -> Unit): Nothing {
try {
suspendCancellableCoroutine<Nothing> { }
} finally {
onDispose()
}
}
}

View File

@@ -28,7 +28,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem
.setMediaMetadata( .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()

View File

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

View File

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