commit
f53057585a
231 changed files with 14460 additions and 9107 deletions
2
.github/workflows/android.yml
vendored
2
.github/workflows/android.yml
vendored
|
@ -2,7 +2,7 @@ name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["master"]
|
branches: ["redesign"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["master"]
|
branches: ["master"]
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
- Background playback
|
- Background playback
|
||||||
- Cache audio chunks for offline playback
|
- Cache audio chunks for offline playback
|
||||||
- Search for songs, albums, artists videos and playlists
|
- Search for songs, albums, artists videos and playlists
|
||||||
|
- Bookmark artists and albums
|
||||||
|
- Import playlists
|
||||||
- Fetch, display and edit songs lyrics or synchronized lyrics
|
- Fetch, display and edit songs lyrics or synchronized lyrics
|
||||||
- Local playlist management
|
- Local playlist management
|
||||||
- Reorder songs in playlist or queue
|
- Reorder songs in playlist or queue
|
||||||
|
@ -29,7 +31,7 @@
|
||||||
- Sleep timer
|
- Sleep timer
|
||||||
- Audio normalization
|
- Audio normalization
|
||||||
- Persistent queue
|
- Persistent queue
|
||||||
- Open YouTube/YouTube Music links (`watch`, `playlist`)
|
- Open YouTube/YouTube Music links (`watch`, `playlist`, `channel`)
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
|
@ -5,18 +5,14 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
signingConfigs {
|
|
||||||
create("release") {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileSdk = 33
|
compileSdk = 33
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "it.vfsfitvnm.vimusic"
|
applicationId = "it.vfsfitvnm.vimusic"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 32
|
targetSdk = 32
|
||||||
versionCode = 15
|
versionCode = 16
|
||||||
versionName = "0.4.3"
|
versionName = "0.5.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
|
@ -93,7 +89,7 @@ dependencies {
|
||||||
kapt(libs.room.compiler)
|
kapt(libs.room.compiler)
|
||||||
annotationProcessor(libs.room.compiler)
|
annotationProcessor(libs.room.compiler)
|
||||||
|
|
||||||
implementation(projects.youtubeMusic)
|
implementation(projects.innertube)
|
||||||
implementation(projects.kugou)
|
implementation(projects.kugou)
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.desugaring)
|
coreLibraryDesugaring(libs.desugaring)
|
||||||
|
|
610
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json
Normal file
610
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json
Normal file
|
@ -0,0 +1,610 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 18,
|
||||||
|
"identityHash": "c8f776e899b181081f0230bffec99ac5",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 'c8f776e899b181081f0230bffec99ac5')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
670
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json
Normal file
670
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json
Normal file
|
@ -0,0 +1,670 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 19,
|
||||||
|
"identityHash": "b9a9bb1674c7c50be2fab48de5afed43",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Event",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playTime",
|
||||||
|
"columnName": "playTime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Event_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Event_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"viewName": "SortedSongPlaylistMap",
|
||||||
|
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b9a9bb1674c7c50be2fab48de5afed43')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
670
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json
Normal file
670
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json
Normal file
|
@ -0,0 +1,670 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 20,
|
||||||
|
"identityHash": "251e713953aacd84fd33b471ed4af391",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Song",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistsText",
|
||||||
|
"columnName": "artistsText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "durationText",
|
||||||
|
"columnName": "durationText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lyrics",
|
||||||
|
"columnName": "lyrics",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "synchronizedLyrics",
|
||||||
|
"columnName": "synchronizedLyrics",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "likedAt",
|
||||||
|
"columnName": "likedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "totalPlayTimeMs",
|
||||||
|
"columnName": "totalPlayTimeMs",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SongPlaylistMap",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistId",
|
||||||
|
"columnName": "playlistId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId",
|
||||||
|
"playlistId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SongPlaylistMap_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_SongPlaylistMap_playlistId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlistId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "Playlist",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"playlistId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Playlist",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "browseId",
|
||||||
|
"columnName": "browseId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Artist",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Event",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playTime",
|
||||||
|
"columnName": "playTime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Event_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Event_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"viewName": "SortedSongPlaylistMap",
|
||||||
|
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '251e713953aacd84fd33b471ed4af391')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
646
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/21.json
Normal file
646
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/21.json
Normal file
|
@ -0,0 +1,646 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 21,
|
||||||
|
"identityHash": "5afda34f61cc45ecd6102a7285ec92d2",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "Song",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistsText",
|
||||||
|
"columnName": "artistsText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "durationText",
|
||||||
|
"columnName": "durationText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lyrics",
|
||||||
|
"columnName": "lyrics",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "synchronizedLyrics",
|
||||||
|
"columnName": "synchronizedLyrics",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "likedAt",
|
||||||
|
"columnName": "likedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "totalPlayTimeMs",
|
||||||
|
"columnName": "totalPlayTimeMs",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SongPlaylistMap",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistId",
|
||||||
|
"columnName": "playlistId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId",
|
||||||
|
"playlistId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SongPlaylistMap_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_SongPlaylistMap_playlistId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlistId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "Playlist",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"playlistId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Playlist",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "browseId",
|
||||||
|
"columnName": "browseId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Artist",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `thumbnailUrl` TEXT, `info` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "info",
|
||||||
|
"columnName": "info",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkedAt",
|
||||||
|
"columnName": "bookmarkedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SongArtistMap",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "artistId",
|
||||||
|
"columnName": "artistId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId",
|
||||||
|
"artistId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SongArtistMap_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_SongArtistMap_artistId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"artistId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "Artist",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"artistId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Album",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnailUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "year",
|
||||||
|
"columnName": "year",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorsText",
|
||||||
|
"columnName": "authorsText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shareUrl",
|
||||||
|
"columnName": "shareUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarkedAt",
|
||||||
|
"columnName": "bookmarkedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SongAlbumMap",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "albumId",
|
||||||
|
"columnName": "albumId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId",
|
||||||
|
"albumId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SongAlbumMap_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_SongAlbumMap_albumId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"albumId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "Album",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"albumId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "SearchQuery",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "query",
|
||||||
|
"columnName": "query",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_SearchQuery_query",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"query"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "QueuedMediaItem",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaItem",
|
||||||
|
"columnName": "mediaItem",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "position",
|
||||||
|
"columnName": "position",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Format",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itag",
|
||||||
|
"columnName": "itag",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mimeType",
|
||||||
|
"columnName": "mimeType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bitrate",
|
||||||
|
"columnName": "bitrate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentLength",
|
||||||
|
"columnName": "contentLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastModified",
|
||||||
|
"columnName": "lastModified",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "loudnessDb",
|
||||||
|
"columnName": "loudnessDb",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "Event",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "songId",
|
||||||
|
"columnName": "songId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"columnName": "timestamp",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playTime",
|
||||||
|
"columnName": "playTime",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_Event_songId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_Event_songId` ON `${TABLE_NAME}` (`songId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "Song",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"songId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"viewName": "SortedSongPlaylistMap",
|
||||||
|
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5afda34f61cc45ecd6102a7285ec92d2')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,6 +72,18 @@
|
||||||
android:host="youtu.be"
|
android:host="youtu.be"
|
||||||
android:pathPrefix="/"
|
android:pathPrefix="/"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="music.youtube.com"
|
||||||
|
android:pathPrefix="/channel"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="www.youtube.com"
|
||||||
|
android:pathPrefix="/channel"
|
||||||
|
android:scheme="https" />
|
||||||
|
<data
|
||||||
|
android:host="m.youtube.com"
|
||||||
|
android:pathPrefix="/c"
|
||||||
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import androidx.media3.common.MediaItem
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
|
import androidx.room.DeleteColumn
|
||||||
import androidx.room.DeleteTable
|
import androidx.room.DeleteTable
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
|
@ -28,6 +29,8 @@ import androidx.room.migration.AutoMigrationSpec
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import it.vfsfitvnm.vimusic.enums.AlbumSortBy
|
||||||
|
import it.vfsfitvnm.vimusic.enums.ArtistSortBy
|
||||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
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
|
||||||
|
@ -35,6 +38,7 @@ import it.vfsfitvnm.vimusic.models.Album
|
||||||
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
|
||||||
|
import it.vfsfitvnm.vimusic.models.Event
|
||||||
import it.vfsfitvnm.vimusic.models.Format
|
import it.vfsfitvnm.vimusic.models.Format
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
|
@ -129,6 +133,9 @@ interface Database {
|
||||||
@Query("UPDATE Song SET likedAt = :likedAt WHERE id = :songId")
|
@Query("UPDATE Song SET likedAt = :likedAt WHERE id = :songId")
|
||||||
fun like(songId: String, likedAt: Long?): Int
|
fun like(songId: String, likedAt: Long?): Int
|
||||||
|
|
||||||
|
@Query("UPDATE Song SET durationText = :durationText WHERE id = :songId")
|
||||||
|
fun updateDurationText(songId: String, durationText: String): Int
|
||||||
|
|
||||||
@Query("SELECT lyrics FROM Song WHERE id = :songId")
|
@Query("SELECT lyrics FROM Song WHERE id = :songId")
|
||||||
fun lyrics(songId: String): Flow<String?>
|
fun lyrics(songId: String): Flow<String?>
|
||||||
|
|
||||||
|
@ -144,9 +151,80 @@ interface Database {
|
||||||
@Query("SELECT * FROM Artist WHERE id = :id")
|
@Query("SELECT * FROM Artist WHERE id = :id")
|
||||||
fun artist(id: String): Flow<Artist?>
|
fun artist(id: String): Flow<Artist?>
|
||||||
|
|
||||||
|
@Query("SELECT timestamp FROM Artist WHERE id = :id")
|
||||||
|
fun artistTimestamp(id: String): Long?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY name DESC")
|
||||||
|
fun artistsByNameDesc(): Flow<List<Artist>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY name ASC")
|
||||||
|
fun artistsByNameAsc(): Flow<List<Artist>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID DESC")
|
||||||
|
fun artistsByRowIdDesc(): Flow<List<Artist>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID ASC")
|
||||||
|
fun artistsByRowIdAsc(): Flow<List<Artist>>
|
||||||
|
|
||||||
|
fun artists(sortBy: ArtistSortBy, sortOrder: SortOrder): Flow<List<Artist>> {
|
||||||
|
return when (sortBy) {
|
||||||
|
ArtistSortBy.Name -> when (sortOrder) {
|
||||||
|
SortOrder.Ascending -> artistsByNameAsc()
|
||||||
|
SortOrder.Descending -> artistsByNameDesc()
|
||||||
|
}
|
||||||
|
ArtistSortBy.DateAdded -> when (sortOrder) {
|
||||||
|
SortOrder.Ascending -> artistsByRowIdAsc()
|
||||||
|
SortOrder.Descending -> artistsByRowIdDesc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM Album WHERE id = :id")
|
@Query("SELECT * FROM Album WHERE id = :id")
|
||||||
fun album(id: String): Flow<Album?>
|
fun album(id: String): Flow<Album?>
|
||||||
|
|
||||||
|
@Query("SELECT timestamp FROM Album WHERE id = :id")
|
||||||
|
fun albumTimestamp(id: String): Long?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
fun albumSongs(albumId: String): Flow<List<DetailedSong>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC")
|
||||||
|
fun albumsByTitleAsc(): Flow<List<Album>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY year ASC")
|
||||||
|
fun albumsByYearAsc(): Flow<List<Album>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID ASC")
|
||||||
|
fun albumsByRowIdAsc(): Flow<List<Album>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title DESC")
|
||||||
|
fun albumsByTitleDesc(): Flow<List<Album>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY year DESC")
|
||||||
|
fun albumsByYearDesc(): Flow<List<Album>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID DESC")
|
||||||
|
fun albumsByRowIdDesc(): Flow<List<Album>>
|
||||||
|
|
||||||
|
fun albums(sortBy: AlbumSortBy, sortOrder: SortOrder): Flow<List<Album>> {
|
||||||
|
return when (sortBy) {
|
||||||
|
AlbumSortBy.Title -> when (sortOrder) {
|
||||||
|
SortOrder.Ascending -> albumsByTitleAsc()
|
||||||
|
SortOrder.Descending -> albumsByTitleDesc()
|
||||||
|
}
|
||||||
|
AlbumSortBy.Year -> when (sortOrder) {
|
||||||
|
SortOrder.Ascending -> albumsByYearAsc()
|
||||||
|
SortOrder.Descending -> albumsByYearDesc()
|
||||||
|
}
|
||||||
|
AlbumSortBy.DateAdded -> when (sortOrder) {
|
||||||
|
SortOrder.Ascending -> albumsByRowIdAsc()
|
||||||
|
SortOrder.Descending -> albumsByRowIdDesc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id")
|
@Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id")
|
||||||
fun incrementTotalPlayTimeMs(id: String, addition: Long)
|
fun incrementTotalPlayTimeMs(id: String, addition: Long)
|
||||||
|
|
||||||
|
@ -190,11 +268,6 @@ interface Database {
|
||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
fun artistSongs(artistId: String): Flow<List<DetailedSong>>
|
fun artistSongs(artistId: String): Flow<List<DetailedSong>>
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
|
|
||||||
@RewriteQueriesToDropUnusedColumns
|
|
||||||
fun albumSongs(albumId: String): Flow<List<DetailedSong>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM Format WHERE songId = :songId")
|
@Query("SELECT * FROM Format WHERE songId = :songId")
|
||||||
fun format(songId: String): Flow<Format>
|
fun format(songId: String): Flow<Format>
|
||||||
|
|
||||||
|
@ -220,27 +293,29 @@ interface Database {
|
||||||
@Query("SELECT loudnessDb FROM Format WHERE songId = :songId")
|
@Query("SELECT loudnessDb FROM Format WHERE songId = :songId")
|
||||||
fun loudnessDb(songId: String): Flow<Float?>
|
fun loudnessDb(songId: String): Flow<Float?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query")
|
||||||
|
fun search(query: String): Flow<List<DetailedSong>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT Song.* FROM Event JOIN Song ON Song.id = songId GROUP BY songId ORDER BY SUM(CAST(playTime AS REAL) / (((:now - timestamp) / 86400000) + 1)) DESC LIMIT 1")
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
fun trending(now: Long = System.currentTimeMillis()): Flow<DetailedSong?>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insert(event: Event)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(format: Format)
|
fun insert(format: Format)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(searchQuery: SearchQuery)
|
fun insert(searchQuery: SearchQuery)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
fun insert(info: Artist): Long
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
fun insert(info: Album): Long
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun insert(playlist: Playlist): Long
|
fun insert(playlist: Playlist): Long
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun insert(songPlaylistMap: SongPlaylistMap): Long
|
fun insert(songPlaylistMap: SongPlaylistMap): Long
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
|
||||||
fun insert(songAlbumMap: SongAlbumMap): Long
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
fun insert(songArtistMap: SongArtistMap): Long
|
fun insert(songArtistMap: SongArtistMap): Long
|
||||||
|
|
||||||
|
@ -250,62 +325,49 @@ interface Database {
|
||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
fun insert(queuedMediaItems: List<QueuedMediaItem>)
|
fun insert(queuedMediaItems: List<QueuedMediaItem>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
fun insertSongPlaylistMaps(songPlaylistMaps: List<SongPlaylistMap>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
fun insert(album: Album, songAlbumMap: SongAlbumMap)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
fun insert(artists: List<Artist>, songArtistMaps: List<SongArtistMap>)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
fun insert(mediaItem: MediaItem, block: (Song) -> Song = { it }) {
|
fun insert(mediaItem: MediaItem, block: (Song) -> Song = { it }) {
|
||||||
val song = Song(
|
val song = Song(
|
||||||
id = mediaItem.mediaId,
|
id = mediaItem.mediaId,
|
||||||
title = mediaItem.mediaMetadata.title!!.toString(),
|
title = mediaItem.mediaMetadata.title!!.toString(),
|
||||||
artistsText = mediaItem.mediaMetadata.artist?.toString(),
|
artistsText = mediaItem.mediaMetadata.artist?.toString(),
|
||||||
durationText = mediaItem.mediaMetadata.extras?.getString("durationText")!!,
|
durationText = mediaItem.mediaMetadata.extras?.getString("durationText"),
|
||||||
thumbnailUrl = mediaItem.mediaMetadata.artworkUri?.toString()
|
thumbnailUrl = mediaItem.mediaMetadata.artworkUri?.toString()
|
||||||
).let(block).also { song ->
|
).let(block).also { song ->
|
||||||
if (insert(song) == -1L) return
|
if (insert(song) == -1L) return
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId ->
|
mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId ->
|
||||||
Album(
|
insert(
|
||||||
id = albumId,
|
Album(id = albumId, title = mediaItem.mediaMetadata.albumTitle?.toString()),
|
||||||
title = mediaItem.mediaMetadata.albumTitle?.toString(),
|
SongAlbumMap(songId = song.id, albumId = albumId, position = null)
|
||||||
year = null,
|
|
||||||
authorsText = null,
|
|
||||||
thumbnailUrl = null,
|
|
||||||
shareUrl = null,
|
|
||||||
timestamp = null,
|
|
||||||
).also(::insert)
|
|
||||||
|
|
||||||
upsert(
|
|
||||||
SongAlbumMap(
|
|
||||||
songId = song.id,
|
|
||||||
albumId = albumId,
|
|
||||||
position = null
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames ->
|
mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames ->
|
||||||
mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds")?.let { artistIds ->
|
mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds")?.let { artistIds ->
|
||||||
artistNames.mapIndexed { index, artistName ->
|
if (artistNames.size == artistIds.size) {
|
||||||
Artist(
|
|
||||||
id = artistIds[index],
|
|
||||||
name = artistName,
|
|
||||||
thumbnailUrl = null,
|
|
||||||
info = null,
|
|
||||||
timestamp = null,
|
|
||||||
).also(::insert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.forEach { artist ->
|
|
||||||
insert(
|
insert(
|
||||||
SongArtistMap(
|
artistNames.mapIndexed { index, artistName ->
|
||||||
songId = song.id,
|
Artist(id = artistIds[index], name = artistName)
|
||||||
artistId = artist.id
|
},
|
||||||
)
|
artistIds.map { artistId ->
|
||||||
|
SongArtistMap(songId = song.id, artistId = artistId)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Update
|
}
|
||||||
fun update(song: Song)
|
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
fun update(artist: Artist)
|
fun update(artist: Artist)
|
||||||
|
@ -313,12 +375,6 @@ interface Database {
|
||||||
@Update
|
@Update
|
||||||
fun update(album: Album)
|
fun update(album: Album)
|
||||||
|
|
||||||
@Update
|
|
||||||
fun update(songAlbumMap: SongAlbumMap)
|
|
||||||
|
|
||||||
@Update
|
|
||||||
fun update(songPlaylistMap: SongPlaylistMap)
|
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
fun update(playlist: Playlist)
|
fun update(playlist: Playlist)
|
||||||
|
|
||||||
|
@ -337,9 +393,6 @@ interface Database {
|
||||||
@Delete
|
@Delete
|
||||||
fun delete(playlist: Playlist)
|
fun delete(playlist: Playlist)
|
||||||
|
|
||||||
@Delete
|
|
||||||
fun delete(playlist: Album)
|
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
fun delete(songPlaylistMap: SongPlaylistMap)
|
fun delete(songPlaylistMap: SongPlaylistMap)
|
||||||
}
|
}
|
||||||
|
@ -356,11 +409,12 @@ interface Database {
|
||||||
SearchQuery::class,
|
SearchQuery::class,
|
||||||
QueuedMediaItem::class,
|
QueuedMediaItem::class,
|
||||||
Format::class,
|
Format::class,
|
||||||
|
Event::class,
|
||||||
],
|
],
|
||||||
views = [
|
views = [
|
||||||
SortedSongPlaylistMap::class
|
SortedSongPlaylistMap::class
|
||||||
],
|
],
|
||||||
version = 17,
|
version = 21,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
|
@ -376,6 +430,10 @@ interface Database {
|
||||||
AutoMigration(from = 13, to = 14),
|
AutoMigration(from = 13, to = 14),
|
||||||
AutoMigration(from = 15, to = 16),
|
AutoMigration(from = 15, to = 16),
|
||||||
AutoMigration(from = 16, to = 17),
|
AutoMigration(from = 16, to = 17),
|
||||||
|
AutoMigration(from = 17, to = 18),
|
||||||
|
AutoMigration(from = 18, to = 19),
|
||||||
|
AutoMigration(from = 19, to = 20),
|
||||||
|
AutoMigration(from = 20, to = 21, spec = DatabaseInitializer.From20To21Migration::class),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
@ -509,6 +567,14 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
|
||||||
it.execSQL("ALTER TABLE Song_new RENAME TO Song;")
|
it.execSQL("ALTER TABLE Song_new RENAME TO Song;")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteColumn.Entries(
|
||||||
|
DeleteColumn("Artist", "shuffleVideoId"),
|
||||||
|
DeleteColumn("Artist", "shufflePlaylistId"),
|
||||||
|
DeleteColumn("Artist", "radioVideoId"),
|
||||||
|
DeleteColumn("Artist", "radioPlaylistId"),
|
||||||
|
)
|
||||||
|
class From20To21Migration : AutoMigrationSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverters
|
@TypeConverters
|
||||||
|
|
|
@ -6,10 +6,10 @@ import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
@ -19,14 +19,14 @@ import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.add
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.material.ripple.LocalRippleTheme
|
import androidx.compose.material.ripple.LocalRippleTheme
|
||||||
import androidx.compose.material.ripple.RippleAlpha
|
import androidx.compose.material.ripple.RippleAlpha
|
||||||
|
@ -35,20 +35,24 @@ import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.neverEqualPolicy
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
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.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.coerceIn
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import com.valentinilk.shimmer.LocalShimmerTheme
|
import com.valentinilk.shimmer.LocalShimmerTheme
|
||||||
|
@ -59,36 +63,39 @@ import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
import it.vfsfitvnm.vimusic.service.PlayerService
|
import it.vfsfitvnm.vimusic.service.PlayerService
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
import it.vfsfitvnm.vimusic.ui.components.collapsedAnchor
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.dismissedAnchor
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.expandedAnchor
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
|
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.HomeScreen
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen
|
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.player.Player
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Appearance
|
import it.vfsfitvnm.vimusic.ui.styling.Appearance
|
||||||
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.colorPaletteOf
|
import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf
|
import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.typographyOf
|
import it.vfsfitvnm.vimusic.ui.styling.typographyOf
|
||||||
import it.vfsfitvnm.vimusic.ui.views.PlayerView
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey
|
import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey
|
||||||
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
|
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
import it.vfsfitvnm.vimusic.utils.getEnum
|
||||||
import it.vfsfitvnm.vimusic.utils.intent
|
import it.vfsfitvnm.vimusic.utils.intent
|
||||||
import it.vfsfitvnm.vimusic.utils.listener
|
import it.vfsfitvnm.vimusic.utils.listener
|
||||||
import it.vfsfitvnm.vimusic.utils.preferences
|
import it.vfsfitvnm.vimusic.utils.preferences
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
|
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.playlistPage
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.song
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
companion object {
|
|
||||||
private var alreadyRunning = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private val serviceConnection = object : ServiceConnection {
|
private val serviceConnection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
if (service is PlayerService.Binder) {
|
if (service is PlayerService.Binder) {
|
||||||
|
@ -102,7 +109,6 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var binder by mutableStateOf<PlayerService.Binder?>(null)
|
private var binder by mutableStateOf<PlayerService.Binder?>(null)
|
||||||
private var uri by mutableStateOf<Uri?>(null, neverEqualPolicy())
|
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
@ -120,19 +126,16 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
val playerBottomSheetAnchor = when {
|
val launchedFromNotification = intent?.extras?.getBoolean("expandPlayerBottomSheet") == true
|
||||||
intent?.extras?.getBoolean("expandPlayerBottomSheet") == true -> expandedAnchor
|
|
||||||
alreadyRunning -> collapsedAnchor
|
|
||||||
else -> dismissedAnchor.also { alreadyRunning = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
uri = intent?.data
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val isSystemInDarkTheme = isSystemInDarkTheme()
|
val isSystemInDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
var appearance by remember(isSystemInDarkTheme) {
|
var appearance by rememberSaveable(
|
||||||
|
isSystemInDarkTheme,
|
||||||
|
stateSaver = Appearance.Companion
|
||||||
|
) {
|
||||||
with(preferences) {
|
with(preferences) {
|
||||||
val colorPaletteName = getEnum(colorPaletteNameKey, ColorPaletteName.Dynamic)
|
val colorPaletteName = getEnum(colorPaletteNameKey, ColorPaletteName.Dynamic)
|
||||||
val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System)
|
val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System)
|
||||||
|
@ -230,6 +233,7 @@ class MainActivity : ComponentActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnailRoundnessKey -> {
|
thumbnailRoundnessKey -> {
|
||||||
val thumbnailRoundness =
|
val thumbnailRoundness =
|
||||||
sharedPreferences.getEnum(key, ThumbnailRoundness.Light)
|
sharedPreferences.getEnum(key, ThumbnailRoundness.Light)
|
||||||
|
@ -297,72 +301,129 @@ class MainActivity : ComponentActivity() {
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(appearance.colorPalette.background0)
|
.background(appearance.colorPalette.background0)
|
||||||
) {
|
) {
|
||||||
val paddingValues = WindowInsets.systemBars.asPaddingValues()
|
val density = LocalDensity.current
|
||||||
|
val windowsInsets = WindowInsets.systemBars
|
||||||
|
val bottomDp = with(density) { windowsInsets.getBottom(density).toDp() }
|
||||||
|
|
||||||
val playerBottomSheetState = rememberBottomSheetState(
|
val playerBottomSheetState = rememberBottomSheetState(
|
||||||
dismissedBound = 0.dp,
|
dismissedBound = 0.dp,
|
||||||
collapsedBound = Dimensions.collapsedPlayer + paddingValues.calculateBottomPadding(),
|
collapsedBound = Dimensions.collapsedPlayer + bottomDp,
|
||||||
expandedBound = maxHeight,
|
expandedBound = maxHeight,
|
||||||
initialAnchor = playerBottomSheetAnchor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val playerAwarePaddingValues = if (playerBottomSheetState.isDismissed) {
|
val playerAwareWindowInsets by remember(bottomDp, playerBottomSheetState.value) {
|
||||||
paddingValues
|
derivedStateOf {
|
||||||
} else {
|
val bottom = playerBottomSheetState.value.coerceIn(bottomDp, playerBottomSheetState.collapsedBound)
|
||||||
object : PaddingValues by paddingValues {
|
|
||||||
override fun calculateBottomPadding(): Dp =
|
windowsInsets
|
||||||
paddingValues.calculateBottomPadding() + Dimensions.collapsedPlayer
|
.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
|
||||||
|
.add(WindowInsets(bottom = bottom))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAppearance provides appearance,
|
LocalAppearance provides appearance,
|
||||||
LocalOverscrollConfiguration provides null,
|
LocalIndication provides rememberRipple(bounded = true),
|
||||||
LocalIndication provides rememberRipple(bounded = false),
|
|
||||||
LocalRippleTheme provides rippleTheme,
|
LocalRippleTheme provides rippleTheme,
|
||||||
LocalShimmerTheme provides shimmerTheme,
|
LocalShimmerTheme provides shimmerTheme,
|
||||||
LocalPlayerServiceBinder provides binder,
|
LocalPlayerServiceBinder provides binder,
|
||||||
LocalPlayerAwarePaddingValues provides playerAwarePaddingValues
|
LocalPlayerAwareWindowInsets provides playerAwareWindowInsets
|
||||||
) {
|
) {
|
||||||
when (val uri = uri) {
|
HomeScreen(
|
||||||
null -> {
|
onPlaylistUrl = { url ->
|
||||||
HomeScreen()
|
onNewIntent(Intent.parseUri(url, 0))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
PlayerView(
|
Player(
|
||||||
layoutState = playerBottomSheetState,
|
layoutState = playerBottomSheetState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
|
|
||||||
DisposableEffect(binder?.player) {
|
|
||||||
binder?.player?.listener(object : Player.Listener {
|
|
||||||
override fun onMediaItemTransition(
|
|
||||||
mediaItem: MediaItem?,
|
|
||||||
reason: Int
|
|
||||||
) {
|
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) {
|
|
||||||
playerBottomSheetState.expand(tween(500))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) ?: onDispose { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> IntentUriScreen(uri = uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
BottomSheetMenu(
|
BottomSheetMenu(
|
||||||
state = LocalMenuState.current,
|
state = LocalMenuState.current,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(binder?.player) {
|
||||||
|
val player = binder?.player ?: return@DisposableEffect onDispose { }
|
||||||
|
|
||||||
|
if (player.currentMediaItem == null) {
|
||||||
|
if (!playerBottomSheetState.isDismissed) {
|
||||||
|
playerBottomSheetState.dismiss()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (playerBottomSheetState.isDismissed) {
|
||||||
|
if (launchedFromNotification) {
|
||||||
|
intent.replaceExtras(Bundle())
|
||||||
|
playerBottomSheetState.expandSoft()
|
||||||
|
} else {
|
||||||
|
playerBottomSheetState.collapseSoft()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
player.listener(object : Player.Listener {
|
||||||
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) {
|
||||||
|
playerBottomSheetState.expand(tween(500))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
uri = intent?.data
|
|
||||||
|
val uri = intent?.data ?: return
|
||||||
|
|
||||||
|
intent.data = null
|
||||||
|
this.intent = null
|
||||||
|
|
||||||
|
Toast.makeText(this, "Opening url...", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
when (val path = uri.pathSegments.firstOrNull()) {
|
||||||
|
"playlist" -> uri.getQueryParameter("list")?.let { playlistId ->
|
||||||
|
val browseId = "VL$playlistId"
|
||||||
|
|
||||||
|
if (playlistId.startsWith("OLAK5uy_")) {
|
||||||
|
Innertube.playlistPage(BrowseBody(browseId = browseId))?.getOrNull()?.let {
|
||||||
|
it.songsPage?.items?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId ->
|
||||||
|
albumRoute.ensureGlobal(browseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playlistRoute.ensureGlobal(browseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"channel", "c" -> uri.lastPathSegment?.let { channelId ->
|
||||||
|
artistRoute.ensureGlobal(channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> when {
|
||||||
|
path == "watch" -> uri.getQueryParameter("v")
|
||||||
|
uri.host == "youtu.be" -> path
|
||||||
|
else -> null
|
||||||
|
}?.let { videoId ->
|
||||||
|
Innertube.song(videoId)?.getOrNull()?.let { song ->
|
||||||
|
val binder = snapshotFlow { binder }.filterNotNull().first()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
binder.player.forcePlay(song.asMediaItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setSystemBarAppearance(isDark: Boolean) {
|
private fun setSystemBarAppearance(isDark: Boolean) {
|
||||||
|
@ -385,4 +446,4 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
val LocalPlayerServiceBinder = staticCompositionLocalOf<PlayerService.Binder?> { null }
|
val LocalPlayerServiceBinder = staticCompositionLocalOf<PlayerService.Binder?> { null }
|
||||||
|
|
||||||
val LocalPlayerAwarePaddingValues = staticCompositionLocalOf<PaddingValues> { TODO() }
|
val LocalPlayerAwareWindowInsets = staticCompositionLocalOf<WindowInsets> { TODO() }
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package it.vfsfitvnm.vimusic.enums
|
||||||
|
|
||||||
|
enum class AlbumSortBy {
|
||||||
|
Title,
|
||||||
|
Year,
|
||||||
|
DateAdded
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package it.vfsfitvnm.vimusic.enums
|
||||||
|
|
||||||
|
enum class ArtistSortBy {
|
||||||
|
Name,
|
||||||
|
DateAdded
|
||||||
|
}
|
|
@ -22,11 +22,4 @@ enum class ThumbnailRoundness {
|
||||||
Heavy -> RoundedCornerShape(8.dp)
|
Heavy -> RoundedCornerShape(8.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
val shape: Shape
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
get() = LocalAppearance.current.thumbnailShape
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,11 @@ import androidx.room.PrimaryKey
|
||||||
@Entity
|
@Entity
|
||||||
data class Album(
|
data class Album(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val title: String?,
|
val title: String? = null,
|
||||||
val thumbnailUrl: String? = null,
|
val thumbnailUrl: String? = null,
|
||||||
val year: String? = null,
|
val year: String? = null,
|
||||||
val authorsText: String? = null,
|
val authorsText: String? = null,
|
||||||
val shareUrl: String? = null,
|
val shareUrl: String? = null,
|
||||||
val timestamp: Long?
|
val timestamp: Long? = null,
|
||||||
|
val bookmarkedAt: Long? = null
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,12 +8,9 @@ import androidx.room.PrimaryKey
|
||||||
@Entity
|
@Entity
|
||||||
data class Artist(
|
data class Artist(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val name: String,
|
val name: String? = null,
|
||||||
val thumbnailUrl: String?,
|
val thumbnailUrl: String? = null,
|
||||||
val info: String?,
|
val info: String? = null,
|
||||||
val shuffleVideoId: String? = null,
|
val timestamp: Long? = null,
|
||||||
val shufflePlaylistId: String? = null,
|
val bookmarkedAt: Long? = null,
|
||||||
val radioVideoId: String? = null,
|
|
||||||
val radioPlaylistId: String? = null,
|
|
||||||
val timestamp: Long?
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,7 @@ open class DetailedSong(
|
||||||
val id: String,
|
val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val artistsText: String? = null,
|
val artistsText: String? = null,
|
||||||
val durationText: String,
|
val durationText: String?,
|
||||||
val thumbnailUrl: String?,
|
val thumbnailUrl: String?,
|
||||||
val totalPlayTimeMs: Long = 0,
|
val totalPlayTimeMs: Long = 0,
|
||||||
@Relation(
|
@Relation(
|
||||||
|
|
25
app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt
Normal file
25
app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package it.vfsfitvnm.vimusic.models
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@Entity(
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = Song::class,
|
||||||
|
parentColumns = ["id"],
|
||||||
|
childColumns = ["songId"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class Event(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
|
@ColumnInfo(index = true) val songId: String,
|
||||||
|
val timestamp: Long,
|
||||||
|
val playTime: Long
|
||||||
|
)
|
|
@ -2,5 +2,5 @@ package it.vfsfitvnm.vimusic.models
|
||||||
|
|
||||||
data class Info(
|
data class Info(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String
|
val name: String?
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,9 +19,4 @@ data class PlaylistWithSongs(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val songs: List<DetailedSong>
|
val songs: List<DetailedSong>
|
||||||
) {
|
)
|
||||||
companion object {
|
|
||||||
val Empty = PlaylistWithSongs(Playlist(-1, ""), emptyList())
|
|
||||||
val NotFound = PlaylistWithSongs(Playlist(-2, "Not found"), emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ data class Song(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val artistsText: String? = null,
|
val artistsText: String? = null,
|
||||||
val durationText: String,
|
val durationText: String?,
|
||||||
val thumbnailUrl: String?,
|
val thumbnailUrl: String?,
|
||||||
val lyrics: String? = null,
|
val lyrics: String? = null,
|
||||||
val synchronizedLyrics: String? = null,
|
val synchronizedLyrics: String? = null,
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
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?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val AlbumListSaver = listSaver(AlbumSaver)
|
|
@ -0,0 +1,27 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
|
|
||||||
|
object ArtistSaver : Saver<Artist, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Artist): List<Any?> = listOf(
|
||||||
|
value.id,
|
||||||
|
value.name,
|
||||||
|
value.thumbnailUrl,
|
||||||
|
value.info,
|
||||||
|
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?,
|
||||||
|
timestamp = value[4] as Long?,
|
||||||
|
bookmarkedAt = value[5] as Long?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ArtistListSaver = listSaver(ArtistSaver)
|
|
@ -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) =
|
||||||
|
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(
|
||||||
|
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 = (value[7] as List<List<String>>?)?.let(InfoListSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val DetailedSongListSaver = listSaver(DetailedSongSaver)
|
13
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt
Normal file
13
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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) = listOf(value.id, value.name)
|
||||||
|
|
||||||
|
override fun restore(value: List<String?>) = Info(id = value[0] as String, name = value[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
val InfoListSaver = listSaver(InfoSaver)
|
|
@ -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.Innertube
|
||||||
|
|
||||||
|
object InnertubeAlbumItemSaver : Saver<Innertube.AlbumItem, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.AlbumItem): List<Any?> = listOf(
|
||||||
|
value.info?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
|
||||||
|
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
|
||||||
|
value.year,
|
||||||
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.AlbumItem(
|
||||||
|
info = (value[0] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
|
||||||
|
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
|
||||||
|
year = value[2] as String?,
|
||||||
|
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val InnertubeAlbumItemListSaver = listSaver(InnertubeAlbumItemSaver)
|
|
@ -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.Innertube
|
||||||
|
|
||||||
|
object InnertubeArtistItemSaver : Saver<Innertube.ArtistItem, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.ArtistItem): List<Any?> = listOf(
|
||||||
|
value.info?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
|
||||||
|
value.subscribersCountText,
|
||||||
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.ArtistItem(
|
||||||
|
info = (value[0] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
|
||||||
|
subscribersCountText = value[1] as String?,
|
||||||
|
thumbnail = (value[2] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val InnertubeArtistItemListSaver = listSaver(InnertubeArtistItemSaver)
|
|
@ -0,0 +1,36 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
object InnertubeArtistPageSaver : Saver<Innertube.ArtistPage, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.ArtistPage) = listOf(
|
||||||
|
value.name,
|
||||||
|
value.description,
|
||||||
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } },
|
||||||
|
value.shuffleEndpoint?.let { with(InnertubeWatchEndpointSaver) { save(it) } },
|
||||||
|
value.radioEndpoint?.let { with(InnertubeWatchEndpointSaver) { save(it) } },
|
||||||
|
value.songs?.let { with(InnertubeSongItemListSaver) { save(it) } },
|
||||||
|
value.songsEndpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } },
|
||||||
|
value.albums?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
|
||||||
|
value.albumsEndpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } },
|
||||||
|
value.singles?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
|
||||||
|
value.singlesEndpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } },
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.ArtistPage(
|
||||||
|
name = value[0] as String?,
|
||||||
|
description = value[1] as String?,
|
||||||
|
thumbnail = (value[2] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore),
|
||||||
|
shuffleEndpoint = (value[3] as List<Any?>?)?.let(InnertubeWatchEndpointSaver::restore),
|
||||||
|
radioEndpoint = (value[4] as List<Any?>?)?.let(InnertubeWatchEndpointSaver::restore),
|
||||||
|
songs = (value[5] as List<List<Any?>>?)?.let(InnertubeSongItemListSaver::restore),
|
||||||
|
songsEndpoint = (value[6] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore),
|
||||||
|
albums = (value[7] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
|
||||||
|
albumsEndpoint = (value[8] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore),
|
||||||
|
singles = (value[9] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
|
||||||
|
singlesEndpoint = (value[10] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
object InnertubeBrowseEndpointSaver : Saver<NavigationEndpoint.Endpoint.Browse, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf(
|
||||||
|
value.browseId,
|
||||||
|
value.params
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = NavigationEndpoint.Endpoint.Browse(
|
||||||
|
browseId = value[0] as String,
|
||||||
|
params = value[1] as String?,
|
||||||
|
browseEndpointContextSupportedConfigs = null
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
object InnertubeBrowseInfoSaver : Saver<Innertube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
|
||||||
|
value.name,
|
||||||
|
value.endpoint?.let { with(InnertubeBrowseEndpointSaver) { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.Info(
|
||||||
|
name = value[0] as String?,
|
||||||
|
endpoint = (value[1] as List<Any?>?)?.let(InnertubeBrowseEndpointSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val InnertubeBrowseInfoListSaver = listSaver(InnertubeBrowseInfoSaver)
|
|
@ -0,0 +1,4 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
val InnertubeSongsPageSaver = innertubeItemsPageSaver(InnertubeSongItemListSaver)
|
||||||
|
val InnertubeAlbumsPageSaver = innertubeItemsPageSaver(InnertubeAlbumItemListSaver)
|
|
@ -0,0 +1,23 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
object InnertubePlaylistItemSaver : Saver<Innertube.PlaylistItem, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.PlaylistItem): List<Any?> = listOf(
|
||||||
|
value.info?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
|
||||||
|
value.channel?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
|
||||||
|
value.songCount,
|
||||||
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.PlaylistItem(
|
||||||
|
info = (value[0] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
|
||||||
|
channel = (value[1] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
|
||||||
|
songCount = value[2] as Int?,
|
||||||
|
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val InnertubePlaylistItemListSaver = listSaver(InnertubePlaylistItemSaver)
|
|
@ -0,0 +1,28 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
object InnertubePlaylistOrAlbumPageSaver : Saver<Innertube.PlaylistOrAlbumPage, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.PlaylistOrAlbumPage): List<Any?> = listOf(
|
||||||
|
value.title,
|
||||||
|
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
|
||||||
|
value.year,
|
||||||
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } } ,
|
||||||
|
value.url,
|
||||||
|
value.songsPage?.let { with(InnertubeSongsPageSaver) { save(it) } },
|
||||||
|
value.otherVersions?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.PlaylistOrAlbumPage(
|
||||||
|
title = value[0] as String?,
|
||||||
|
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
|
||||||
|
year = value[2] as String?,
|
||||||
|
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore),
|
||||||
|
url = value[4] as String?,
|
||||||
|
songsPage = (value[5] as List<Any?>?)?.let(InnertubeSongsPageSaver::restore),
|
||||||
|
otherVersions = (value[6] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
|
||||||
|
)
|
||||||
|
}
|
|
@ -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.Innertube
|
||||||
|
|
||||||
|
object InnertubeRelatedPageSaver : Saver<Innertube.RelatedPage, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.RelatedPage): List<Any?> = listOf(
|
||||||
|
value.songs?.let { with(InnertubeSongItemListSaver) { save(it) } },
|
||||||
|
value.playlists?.let { with(InnertubePlaylistItemListSaver) { save(it) } },
|
||||||
|
value.albums?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
|
||||||
|
value.artists?.let { with(InnertubeArtistItemListSaver) { save(it) } },
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.RelatedPage(
|
||||||
|
songs = (value[0] as List<List<Any?>>?)?.let(InnertubeSongItemListSaver::restore),
|
||||||
|
playlists = (value[1] as List<List<Any?>>?)?.let(InnertubePlaylistItemListSaver::restore),
|
||||||
|
albums = (value[2] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
|
||||||
|
artists = (value[3] as List<List<Any?>>?)?.let(InnertubeArtistItemListSaver::restore),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
object InnertubeSongItemSaver : Saver<Innertube.SongItem, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.SongItem): List<Any?> = listOf(
|
||||||
|
value.info?.let { with(InnertubeWatchInfoSaver) { save(it) } },
|
||||||
|
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
|
||||||
|
value.album?.let { with(InnertubeBrowseInfoSaver) { save(it) } },
|
||||||
|
value.durationText,
|
||||||
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.SongItem(
|
||||||
|
info = (value[0] as List<Any?>?)?.let(InnertubeWatchInfoSaver::restore),
|
||||||
|
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
|
||||||
|
album = (value[2] as List<Any?>?)?.let(InnertubeBrowseInfoSaver::restore),
|
||||||
|
durationText = value[3] as String?,
|
||||||
|
thumbnail = (value[4] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val InnertubeSongItemListSaver = listSaver(InnertubeSongItemSaver)
|
|
@ -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.Thumbnail
|
||||||
|
|
||||||
|
object InnertubeThumbnailSaver : Saver<Thumbnail, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Thumbnail) = listOf(
|
||||||
|
value.url,
|
||||||
|
value.width,
|
||||||
|
value.height
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = Thumbnail(
|
||||||
|
url = value[0] as String,
|
||||||
|
width = value[1] as Int,
|
||||||
|
height = value[2] as Int?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
object InnertubeVideoItemSaver : Saver<Innertube.VideoItem, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.VideoItem): List<Any?> = listOf(
|
||||||
|
value.info?.let { with(InnertubeWatchInfoSaver) { save(it) } },
|
||||||
|
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
|
||||||
|
value.viewsText,
|
||||||
|
value.durationText,
|
||||||
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.VideoItem(
|
||||||
|
info = (value[0] as List<Any?>?)?.let(InnertubeWatchInfoSaver::restore),
|
||||||
|
authors = (value[1] as List<List<Any?>>?)?.let(InnertubeBrowseInfoListSaver::restore),
|
||||||
|
viewsText = value[2] as String?,
|
||||||
|
durationText = value[3] as String?,
|
||||||
|
thumbnail = (value[4] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val InnertubeVideoItemListSaver = listSaver(InnertubeVideoItemSaver)
|
|
@ -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 InnertubeWatchEndpointSaver : Saver<NavigationEndpoint.Endpoint.Watch, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf(
|
||||||
|
value.params,
|
||||||
|
value.playlistId,
|
||||||
|
value.videoId,
|
||||||
|
value.index,
|
||||||
|
value.playlistSetVideoId,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = NavigationEndpoint.Endpoint.Watch(
|
||||||
|
params = value[0] as String?,
|
||||||
|
playlistId = value[1] as String?,
|
||||||
|
videoId = value[2] as String?,
|
||||||
|
index = value[3] as Int?,
|
||||||
|
playlistSetVideoId = value[4] as String?,
|
||||||
|
watchEndpointMusicSupportedConfigs = null
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
object InnertubeWatchInfoSaver : Saver<Innertube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
|
||||||
|
value.name,
|
||||||
|
value.endpoint?.let { with(InnertubeWatchEndpointSaver) { save(it) } },
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.Info(
|
||||||
|
name = value[0] as String?,
|
||||||
|
endpoint = (value[1] as List<Any?>?)?.let(InnertubeWatchEndpointSaver::restore)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
|
|
||||||
|
object PlaylistPreviewSaver : Saver<PlaylistPreview, List<Any>> {
|
||||||
|
override fun SaverScope.save(value: PlaylistPreview) = listOf(
|
||||||
|
with(PlaylistSaver) { save(value.playlist) },
|
||||||
|
value.songCount,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any>) = PlaylistPreview(
|
||||||
|
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
||||||
|
songCount = value[1] as Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val PlaylistPreviewListSaver = listSaver(PlaylistPreviewSaver)
|
|
@ -0,0 +1,19 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
|
|
||||||
|
object PlaylistSaver : Saver<Playlist, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Playlist): List<Any?> = listOf(
|
||||||
|
value.id,
|
||||||
|
value.name,
|
||||||
|
value.browseId,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun restore(value: List<Any?>): Playlist = Playlist(
|
||||||
|
id = value[0] as Long,
|
||||||
|
name = value[1] as String,
|
||||||
|
browseId = value[2] as String?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
||||||
|
|
||||||
|
object PlaylistWithSongsSaver : Saver<PlaylistWithSongs, List<Any>> {
|
||||||
|
override fun SaverScope.save(value: PlaylistWithSongs) = listOf(
|
||||||
|
with(PlaylistSaver) { save(value.playlist) },
|
||||||
|
with(DetailedSongListSaver) { save(value.songs) },
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any>): PlaylistWithSongs = PlaylistWithSongs(
|
||||||
|
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
||||||
|
songs = DetailedSongListSaver.restore(value[1] as List<List<Any?>>)
|
||||||
|
)
|
||||||
|
}
|
52
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt
Normal file
52
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/Savers.kt
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
|
||||||
|
override fun SaverScope.save(value: List<Original>): List<Saveable>
|
||||||
|
override fun restore(value: List<Saveable>): List<Original>
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <Original, Saveable : Any> resultSaver(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <Original, Saveable : Any> listSaver(saver: Saver<Original, Saveable>) =
|
||||||
|
object : ListSaver<Original, Saveable> {
|
||||||
|
override fun restore(value: List<Saveable>) =
|
||||||
|
value.mapNotNull(saver::restore)
|
||||||
|
|
||||||
|
override fun SaverScope.save(value: List<Original>) =
|
||||||
|
with(saver) { value.mapNotNull { save(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <Original, Saveable : Any> nullableSaver(saver: Saver<Original, Saveable>) =
|
||||||
|
object : Saver<Original?, Saveable> {
|
||||||
|
override fun SaverScope.save(value: Original?): Saveable? =
|
||||||
|
value?.let { with(saver) { save(it) } }
|
||||||
|
|
||||||
|
override fun restore(value: Saveable): Original? =
|
||||||
|
saver.restore(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <Original : Innertube.Item> innertubeItemsPageSaver(saver: ListSaver<Original, List<Any?>>) =
|
||||||
|
object : Saver<Innertube.ItemsPage<Original>, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: Innertube.ItemsPage<Original>) = listOf(
|
||||||
|
value.items?.let { with(saver) { save(it) } },
|
||||||
|
value.continuation
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = Innertube.ItemsPage(
|
||||||
|
items = (value[0] as List<List<Any?>>?)?.let(saver::restore),
|
||||||
|
continuation = value[1] as String?
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import android.media.session.PlaybackState
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.text.format.DateUtils
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
@ -65,6 +66,7 @@ import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.MainActivity
|
import it.vfsfitvnm.vimusic.MainActivity
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
|
import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
|
||||||
|
import it.vfsfitvnm.vimusic.models.Event
|
||||||
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
|
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.utils.InvincibleService
|
import it.vfsfitvnm.vimusic.utils.InvincibleService
|
||||||
|
@ -90,8 +92,10 @@ import it.vfsfitvnm.vimusic.utils.shouldBePlaying
|
||||||
import it.vfsfitvnm.vimusic.utils.skipSilenceKey
|
import it.vfsfitvnm.vimusic.utils.skipSilenceKey
|
||||||
import it.vfsfitvnm.vimusic.utils.timer
|
import it.vfsfitvnm.vimusic.utils.timer
|
||||||
import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey
|
import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.PlayerBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.player
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -100,6 +104,7 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.cancellable
|
import kotlinx.coroutines.flow.cancellable
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -285,11 +290,26 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
|
|
||||||
val totalPlayTimeMs = playbackStats.totalPlayTimeMs
|
val totalPlayTimeMs = playbackStats.totalPlayTimeMs
|
||||||
|
|
||||||
if (totalPlayTimeMs > 2000) {
|
if (totalPlayTimeMs > 5000) {
|
||||||
query {
|
query {
|
||||||
Database.incrementTotalPlayTimeMs(mediaItem.mediaId, totalPlayTimeMs)
|
Database.incrementTotalPlayTimeMs(mediaItem.mediaId, totalPlayTimeMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (totalPlayTimeMs > 30000) {
|
||||||
|
query {
|
||||||
|
// THANKS, EXOPLAYER
|
||||||
|
if (runBlocking { Database.song(mediaItem.mediaId).first() } != null) {
|
||||||
|
Database.insert(
|
||||||
|
Event(
|
||||||
|
songId = mediaItem.mediaId,
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
playTime = totalPlayTimeMs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
@ -628,9 +648,9 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
|
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
|
||||||
else -> {
|
else -> {
|
||||||
val urlResult = runBlocking(Dispatchers.IO) {
|
val urlResult = runBlocking(Dispatchers.IO) {
|
||||||
YouTube.player(videoId)
|
Innertube.player(PlayerBody(videoId = videoId))
|
||||||
}?.mapCatching { body ->
|
}?.mapCatching { body ->
|
||||||
when (val status = body.playabilityStatus.status) {
|
when (val status = body.playabilityStatus?.status) {
|
||||||
"OK" -> body.streamingData?.adaptiveFormats?.findLast { format ->
|
"OK" -> body.streamingData?.adaptiveFormats?.findLast { format ->
|
||||||
format.itag == 251 || format.itag == 140
|
format.itag == 251 || format.itag == 140
|
||||||
}?.let { format ->
|
}?.let { format ->
|
||||||
|
@ -638,6 +658,13 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
||||||
player.findNextMediaItemById(videoId)
|
player.findNextMediaItemById(videoId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mediaItem?.mediaMetadata?.extras?.getString("durationText") == null) {
|
||||||
|
format.approxDurationMs?.div(1000)?.let(DateUtils::formatElapsedTime)?.removePrefix("0")?.let { durationText ->
|
||||||
|
mediaItem?.mediaMetadata?.extras?.putString("durationText", durationText)
|
||||||
|
Database.updateDurationText(videoId, durationText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query {
|
query {
|
||||||
mediaItem?.let(Database::insert)
|
mediaItem?.let(Database::insert)
|
||||||
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
fun Modifier.badge(color: Color, isDisplayed: Boolean = true, radius: Dp = 4.dp) =
|
|
||||||
if (isDisplayed) {
|
|
||||||
drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawCircle(
|
|
||||||
color = color,
|
|
||||||
center = Offset(x = size.width, y = 0.dp.toPx()),
|
|
||||||
radius = radius.toPx()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
|
@ -10,14 +10,12 @@ import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.DraggableState
|
import androidx.compose.foundation.gestures.DraggableState
|
||||||
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
@ -40,7 +38,6 @@ import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -74,36 +71,7 @@ fun BottomSheet(
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
val velocity = -velocityTracker.calculateVelocity().y
|
val velocity = -velocityTracker.calculateVelocity().y
|
||||||
velocityTracker.resetTracking()
|
velocityTracker.resetTracking()
|
||||||
|
state.performFling(velocity, onDismiss)
|
||||||
if (velocity > 250) {
|
|
||||||
state.expand()
|
|
||||||
} else if (velocity < -250) {
|
|
||||||
if (state.value < state.collapsedBound && onDismiss != null) {
|
|
||||||
state.dismiss()
|
|
||||||
onDismiss.invoke()
|
|
||||||
} else {
|
|
||||||
state.collapse()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val l0 = state.dismissedBound
|
|
||||||
val l1 = (state.collapsedBound - state.dismissedBound) / 2
|
|
||||||
val l2 = (state.expandedBound - state.collapsedBound) / 2
|
|
||||||
val l3 = state.expandedBound
|
|
||||||
|
|
||||||
when (state.value) {
|
|
||||||
in l0..l1 -> {
|
|
||||||
if (onDismiss != null) {
|
|
||||||
state.dismiss()
|
|
||||||
onDismiss.invoke()
|
|
||||||
} else {
|
|
||||||
state.collapse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
in l1..l2 -> state.collapse()
|
|
||||||
in l2..l3 -> state.expand()
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -120,11 +88,7 @@ fun BottomSheet(
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
alpha = 1f - (state.progress * 16).coerceAtMost(1f)
|
alpha = 1f - (state.progress * 16).coerceAtMost(1f)
|
||||||
}
|
}
|
||||||
.clickable(
|
.clickable(onClick = state::expandSoft)
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
onClick = state::expandSoft
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(state.collapsedBound),
|
.height(state.collapsedBound),
|
||||||
content = collapsedContent
|
content = collapsedContent
|
||||||
|
@ -179,11 +143,11 @@ class BottomSheetState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapse() {
|
private fun collapse() {
|
||||||
collapse(SpringSpec())
|
collapse(SpringSpec())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expand() {
|
private fun expand() {
|
||||||
expand(SpringSpec())
|
expand(SpringSpec())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,21 +172,53 @@ class BottomSheetState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nestedScrollConnection(initialIsTopReached: Boolean = true): NestedScrollConnection {
|
fun performFling(velocity: Float, onDismiss: (() -> Unit)?) {
|
||||||
return object : NestedScrollConnection {
|
if (velocity > 250) {
|
||||||
var isTopReached = initialIsTopReached
|
expand()
|
||||||
|
} else if (velocity < -250) {
|
||||||
|
if (value < collapsedBound && onDismiss != null) {
|
||||||
|
dismiss()
|
||||||
|
onDismiss.invoke()
|
||||||
|
} else {
|
||||||
|
collapse()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val l0 = dismissedBound
|
||||||
|
val l1 = (collapsedBound - dismissedBound) / 2
|
||||||
|
val l2 = (expandedBound - collapsedBound) / 2
|
||||||
|
val l3 = expandedBound
|
||||||
|
|
||||||
|
when (value) {
|
||||||
|
in l0..l1 -> {
|
||||||
|
if (onDismiss != null) {
|
||||||
|
dismiss()
|
||||||
|
onDismiss.invoke()
|
||||||
|
} else {
|
||||||
|
collapse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in l1..l2 -> collapse()
|
||||||
|
in l2..l3 -> expand()
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val preUpPostDownNestedScrollConnection
|
||||||
|
get() = object : NestedScrollConnection {
|
||||||
|
var isTopReached = false
|
||||||
|
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
if (isExpanded && available.y < 0) {
|
if (isExpanded && available.y < 0) {
|
||||||
isTopReached = false
|
isTopReached = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTopReached) {
|
return if (isTopReached && available.y < 0 && source == NestedScrollSource.Drag) {
|
||||||
dispatchRawDelta(available.y)
|
dispatchRawDelta(available.y)
|
||||||
return available
|
available
|
||||||
|
} else {
|
||||||
|
Offset.Zero
|
||||||
}
|
}
|
||||||
|
|
||||||
return Offset.Zero
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostScroll(
|
override fun onPostScroll(
|
||||||
|
@ -234,42 +230,28 @@ class BottomSheetState(
|
||||||
isTopReached = consumed.y == 0f && available.y > 0
|
isTopReached = consumed.y == 0f && available.y > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return Offset.Zero
|
return if (isTopReached && source == NestedScrollSource.Drag) {
|
||||||
|
dispatchRawDelta(available.y)
|
||||||
|
available
|
||||||
|
} else {
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
if (isTopReached) {
|
return if (isTopReached) {
|
||||||
val velocity = -available.y
|
val velocity = -available.y
|
||||||
coroutineScope {
|
performFling(velocity, null)
|
||||||
if (velocity > 250) {
|
|
||||||
expand()
|
available
|
||||||
} else if (velocity < -250) {
|
|
||||||
collapse()
|
|
||||||
} else {
|
} else {
|
||||||
val l0 = dismissedBound
|
Velocity.Zero
|
||||||
val l1 = (collapsedBound - dismissedBound) / 2
|
|
||||||
val l2 = (expandedBound - collapsedBound) / 2
|
|
||||||
val l3 = expandedBound
|
|
||||||
|
|
||||||
when (value) {
|
|
||||||
in l0..l1 -> collapse()
|
|
||||||
in l1..l2 -> collapse()
|
|
||||||
in l2..l3 -> expand()
|
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return available
|
|
||||||
}
|
|
||||||
|
|
||||||
return Velocity.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
isTopReached = false
|
isTopReached = false
|
||||||
return super.onPostFling(consumed, available)
|
return Velocity.Zero
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ChunkyButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
backgroundColor: Color,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
text: String? = null,
|
|
||||||
secondaryText: String? = null,
|
|
||||||
textStyle: TextStyle = TextStyle.Default,
|
|
||||||
secondaryTextStyle: TextStyle = TextStyle.Default,
|
|
||||||
rippleColor: Color = Color.Unspecified,
|
|
||||||
@DrawableRes icon: Int? = null,
|
|
||||||
shape: Shape = RoundedCornerShape(16.dp),
|
|
||||||
colorFilter: ColorFilter = ColorFilter.tint(rippleColor),
|
|
||||||
isEnabled: Boolean = true,
|
|
||||||
onMore: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = modifier
|
|
||||||
.clip(shape)
|
|
||||||
.background(backgroundColor)
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true, color = rippleColor),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
enabled = isEnabled,
|
|
||||||
onClick = onClick
|
|
||||||
)
|
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
|
||||||
) {
|
|
||||||
icon?.let { icon ->
|
|
||||||
Image(
|
|
||||||
painter = painterResource(icon),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = colorFilter,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
text?.let { text ->
|
|
||||||
Column {
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = textStyle
|
|
||||||
)
|
|
||||||
|
|
||||||
secondaryText?.let { secondaryText ->
|
|
||||||
BasicText(
|
|
||||||
text = secondaryText,
|
|
||||||
style = secondaryTextStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMore?.let { onMore ->
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(rippleColor.copy(alpha = 0.6f))
|
|
||||||
.width(1.dp)
|
|
||||||
.height(24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
// TODO: this is themed...
|
|
||||||
painter = painterResource(R.drawable.ellipsis_vertical),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(rippleColor.copy(alpha = 0.6f)),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onMore)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.horizontalScroll
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> ChipGroup(
|
|
||||||
items: List<ChipItem<T>>,
|
|
||||||
value: T,
|
|
||||||
selectedBackgroundColor: Color,
|
|
||||||
unselectedBackgroundColor: Color,
|
|
||||||
selectedTextStyle: TextStyle,
|
|
||||||
unselectedTextStyle: TextStyle,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = RoundedCornerShape(16.dp),
|
|
||||||
onValueChanged: (T) -> Unit
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.horizontalScroll(rememberScrollState())
|
|
||||||
.then(modifier)
|
|
||||||
) {
|
|
||||||
items.forEach { chipItem ->
|
|
||||||
ChunkyButton(
|
|
||||||
text = chipItem.text,
|
|
||||||
textStyle = if (chipItem.value == value) selectedTextStyle else unselectedTextStyle,
|
|
||||||
backgroundColor = if (chipItem.value == value) selectedBackgroundColor else unselectedBackgroundColor,
|
|
||||||
shape = shape,
|
|
||||||
onClick = {
|
|
||||||
onValueChanged(chipItem.value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ChipItem<T>(
|
|
||||||
val text: String,
|
|
||||||
val value: T
|
|
||||||
)
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShimmerHost(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
|
modifier = modifier
|
||||||
|
.shimmer()
|
||||||
|
.graphicsLayer(alpha = 0.99f)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.verticalGradient(listOf(Color.Black, Color.Transparent)),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
inline fun TopAppBar(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable RowScope.() -> Unit
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -7,7 +7,6 @@ import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -25,7 +24,6 @@ import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -36,7 +34,6 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
@ -51,10 +48,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
|
||||||
import it.vfsfitvnm.vimusic.utils.drawCircle
|
import it.vfsfitvnm.vimusic.utils.drawCircle
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
@ -95,9 +90,7 @@ fun TextFieldDialog(
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = textFieldValue,
|
value = textFieldValue,
|
||||||
onValueChange = {
|
onValueChange = { textFieldValue = it },
|
||||||
textFieldValue = it
|
|
||||||
},
|
|
||||||
textStyle = typography.xs.semiBold.center,
|
textStyle = typography.xs.semiBold.center,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
|
@ -144,19 +137,14 @@ fun TextFieldDialog(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
ChunkyButton(
|
DialogTextButton(
|
||||||
backgroundColor = Color.Transparent,
|
|
||||||
text = cancelText,
|
text = cancelText,
|
||||||
textStyle = typography.xs.semiBold,
|
|
||||||
shape = RoundedCornerShape(36.dp),
|
|
||||||
onClick = onCancel
|
onClick = onCancel
|
||||||
)
|
)
|
||||||
|
|
||||||
ChunkyButton(
|
DialogTextButton(
|
||||||
backgroundColor = colorPalette.accent,
|
primary = true,
|
||||||
text = doneText,
|
text = doneText,
|
||||||
textStyle = typography.xs.semiBold.color(colorPalette.onAccent),
|
|
||||||
shape = RoundedCornerShape(36.dp),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isTextInputValid(textFieldValue.text)) {
|
if (isTextInputValid(textFieldValue.text)) {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
|
@ -183,7 +171,7 @@ fun ConfirmationDialog(
|
||||||
confirmText: String = "Confirm",
|
confirmText: String = "Confirm",
|
||||||
onCancel: () -> Unit = onDismiss
|
onCancel: () -> Unit = onDismiss
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (_, typography) = LocalAppearance.current
|
||||||
|
|
||||||
DefaultDialog(
|
DefaultDialog(
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
|
@ -191,7 +179,7 @@ fun ConfirmationDialog(
|
||||||
) {
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = text,
|
text = text,
|
||||||
style = typography.xs.semiBold.center,
|
style = typography.xs.medium.center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(all = 16.dp)
|
.padding(all = 16.dp)
|
||||||
)
|
)
|
||||||
|
@ -201,19 +189,14 @@ fun ConfirmationDialog(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
ChunkyButton(
|
DialogTextButton(
|
||||||
backgroundColor = Color.Transparent,
|
|
||||||
text = cancelText,
|
text = cancelText,
|
||||||
textStyle = typography.xs.semiBold,
|
|
||||||
shape = RoundedCornerShape(36.dp),
|
|
||||||
onClick = onCancel
|
onClick = onCancel
|
||||||
)
|
)
|
||||||
|
|
||||||
ChunkyButton(
|
DialogTextButton(
|
||||||
backgroundColor = colorPalette.accent,
|
|
||||||
text = confirmText,
|
text = confirmText,
|
||||||
textStyle = typography.xs.semiBold.color(colorPalette.onAccent),
|
primary = true,
|
||||||
shape = RoundedCornerShape(36.dp),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onConfirm()
|
onConfirm()
|
||||||
onDismiss()
|
onDismiss()
|
||||||
|
@ -267,10 +250,7 @@ inline fun <T> ValueSelectorDialog(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(all = 48.dp)
|
.padding(all = 48.dp)
|
||||||
.background(
|
.background(color = colorPalette.background1, shape = RoundedCornerShape(8.dp))
|
||||||
color = colorPalette.background1,
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(vertical = 16.dp),
|
.padding(vertical = 16.dp),
|
||||||
) {
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
|
@ -290,8 +270,6 @@ inline fun <T> ValueSelectorDialog(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
onValueSelected(value)
|
onValueSelected(value)
|
||||||
|
@ -340,20 +318,17 @@ inline fun <T> ValueSelectorDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BasicText(
|
Box(
|
||||||
text = "Cancel",
|
|
||||||
style = typography.xs.semiBold,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 24.dp)
|
|
||||||
.clip(RoundedCornerShape(36.dp))
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = onDismiss
|
|
||||||
)
|
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
|
.padding(end = 24.dp)
|
||||||
|
) {
|
||||||
|
DialogTextButton(
|
||||||
|
text = "Cancel",
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogTextButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
primary: Boolean = false,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
val textColor = when {
|
||||||
|
!enabled -> colorPalette.textDisabled
|
||||||
|
primary -> colorPalette.onAccent
|
||||||
|
else -> colorPalette.text
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = text,
|
||||||
|
style = typography.xs.medium.color(textColor),
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(36.dp))
|
||||||
|
.background(if (primary) colorPalette.accent else Color.Transparent)
|
||||||
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
|
.padding(horizontal = 20.dp, vertical = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,106 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components.themed
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DropDownSection(content: @Composable ColumnScope.() -> Unit) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.shadow(
|
|
||||||
elevation = 2.dp,
|
|
||||||
shape = RoundedCornerShape(16.dp)
|
|
||||||
)
|
|
||||||
.background(colorPalette.background1)
|
|
||||||
.width(IntrinsicSize.Max),
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DropDownSectionSpacer() {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DropDownTextItem(
|
|
||||||
text: String,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = text,
|
|
||||||
textColor = if (isSelected) {
|
|
||||||
colorPalette.onAccent
|
|
||||||
} else {
|
|
||||||
colorPalette.textSecondary
|
|
||||||
},
|
|
||||||
backgroundColor = if (isSelected) {
|
|
||||||
colorPalette.accent
|
|
||||||
} else {
|
|
||||||
colorPalette.background1
|
|
||||||
},
|
|
||||||
onClick = onClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DropDownTextItem(
|
|
||||||
text: String,
|
|
||||||
backgroundColor: Color? = null,
|
|
||||||
textColor: Color? = null,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xxs.medium.copy(
|
|
||||||
color = textColor ?: colorPalette.text,
|
|
||||||
letterSpacing = 1.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = onClick
|
|
||||||
)
|
|
||||||
.background(backgroundColor ?: colorPalette.background1)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.widthIn(min = 124.dp, max = 248.dp)
|
|
||||||
.padding(
|
|
||||||
horizontal = 16.dp,
|
|
||||||
vertical = 8.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,205 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components.themed
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
|
||||||
import androidx.compose.animation.core.animateFloat
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.core.updateTransition
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.unit.Density
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.IntRect
|
|
||||||
import androidx.compose.ui.unit.IntSize
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Popup
|
|
||||||
import androidx.compose.ui.window.PopupPositionProvider
|
|
||||||
import androidx.compose.ui.window.PopupProperties
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DropdownMenu(
|
|
||||||
isDisplayed: Boolean,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
offset: DpOffset = DpOffset(0.dp, 0.dp),
|
|
||||||
properties: PopupProperties = PopupProperties(focusable = true),
|
|
||||||
content: @Composable ColumnScope.() -> Unit
|
|
||||||
) {
|
|
||||||
val expandedStates = remember {
|
|
||||||
MutableTransitionState(false)
|
|
||||||
}.apply { targetState = isDisplayed }
|
|
||||||
|
|
||||||
if (expandedStates.currentState || expandedStates.targetState) {
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
var transformOrigin by remember {
|
|
||||||
mutableStateOf(TransformOrigin.Center)
|
|
||||||
}
|
|
||||||
|
|
||||||
val popupPositionProvider =
|
|
||||||
DropdownMenuPositionProvider(offset, density) { parentBounds, menuBounds ->
|
|
||||||
transformOrigin = calculateTransformOrigin(parentBounds, menuBounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
Popup(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
popupPositionProvider = popupPositionProvider,
|
|
||||||
properties = properties
|
|
||||||
) {
|
|
||||||
DropdownMenuContent(
|
|
||||||
expandedStates = expandedStates,
|
|
||||||
transformOrigin = transformOrigin,
|
|
||||||
modifier = modifier,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun DropdownMenuContent(
|
|
||||||
expandedStates: MutableTransitionState<Boolean>,
|
|
||||||
transformOrigin: TransformOrigin,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable ColumnScope.() -> Unit
|
|
||||||
) {
|
|
||||||
val transition = updateTransition(expandedStates, "DropDownMenu")
|
|
||||||
|
|
||||||
val scale by transition.animateFloat(
|
|
||||||
transitionSpec = {
|
|
||||||
if (false isTransitioningTo true) {
|
|
||||||
// Dismissed to expanded
|
|
||||||
tween(
|
|
||||||
durationMillis = 128,
|
|
||||||
easing = LinearOutSlowInEasing
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Expanded to dismissed.
|
|
||||||
tween(
|
|
||||||
durationMillis = 64,
|
|
||||||
delayMillis = 64
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, label = ""
|
|
||||||
) { isDisplayed ->
|
|
||||||
if (isDisplayed) 1f else 0.9f
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
this.transformOrigin = transformOrigin
|
|
||||||
},
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
private data class DropdownMenuPositionProvider(
|
|
||||||
val contentOffset: DpOffset,
|
|
||||||
val density: Density,
|
|
||||||
val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }
|
|
||||||
) : PopupPositionProvider {
|
|
||||||
override fun calculatePosition(
|
|
||||||
anchorBounds: IntRect,
|
|
||||||
windowSize: IntSize,
|
|
||||||
layoutDirection: LayoutDirection,
|
|
||||||
popupContentSize: IntSize
|
|
||||||
): IntOffset {
|
|
||||||
// The min margin above and below the menu, relative to the screen.
|
|
||||||
val verticalMargin = with(density) { 48.dp.roundToPx() }
|
|
||||||
// The content offset specified using the dropdown offset parameter.
|
|
||||||
val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
|
|
||||||
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
|
|
||||||
|
|
||||||
// Compute horizontal position.
|
|
||||||
val toRight = anchorBounds.left + contentOffsetX
|
|
||||||
val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
|
|
||||||
val toDisplayRight = windowSize.width - popupContentSize.width
|
|
||||||
val toDisplayLeft = 0
|
|
||||||
val x = if (layoutDirection == LayoutDirection.Ltr) {
|
|
||||||
sequenceOf(
|
|
||||||
toRight,
|
|
||||||
toLeft,
|
|
||||||
// If the anchor gets outside of the window on the left, we want to position
|
|
||||||
// toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
|
|
||||||
if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
sequenceOf(
|
|
||||||
toLeft,
|
|
||||||
toRight,
|
|
||||||
// If the anchor gets outside of the window on the right, we want to position
|
|
||||||
// toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
|
|
||||||
if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight
|
|
||||||
)
|
|
||||||
}.firstOrNull {
|
|
||||||
it >= 0 && it + popupContentSize.width <= windowSize.width
|
|
||||||
} ?: toLeft
|
|
||||||
|
|
||||||
// Compute vertical position.
|
|
||||||
val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
|
|
||||||
val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
|
|
||||||
val toCenter = anchorBounds.top - popupContentSize.height / 2
|
|
||||||
val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
|
|
||||||
val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
|
|
||||||
it >= verticalMargin &&
|
|
||||||
it + popupContentSize.height <= windowSize.height - verticalMargin
|
|
||||||
} ?: toTop
|
|
||||||
|
|
||||||
onPositionCalculated(
|
|
||||||
anchorBounds,
|
|
||||||
IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height)
|
|
||||||
)
|
|
||||||
return IntOffset(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun calculateTransformOrigin(
|
|
||||||
parentBounds: IntRect,
|
|
||||||
menuBounds: IntRect
|
|
||||||
): TransformOrigin {
|
|
||||||
val pivotX = when {
|
|
||||||
menuBounds.left >= parentBounds.right -> 0f
|
|
||||||
menuBounds.right <= parentBounds.left -> 1f
|
|
||||||
menuBounds.width == 0 -> 0f
|
|
||||||
else -> {
|
|
||||||
val intersectionCenter =
|
|
||||||
(
|
|
||||||
max(parentBounds.left, menuBounds.left) +
|
|
||||||
min(parentBounds.right, menuBounds.right)
|
|
||||||
) / 2
|
|
||||||
(intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val pivotY = when {
|
|
||||||
menuBounds.top >= parentBounds.bottom -> 0f
|
|
||||||
menuBounds.bottom <= parentBounds.top -> 1f
|
|
||||||
menuBounds.height == 0 -> 0f
|
|
||||||
else -> {
|
|
||||||
val intersectionCenter =
|
|
||||||
(
|
|
||||||
max(parentBounds.top, menuBounds.top) +
|
|
||||||
min(parentBounds.bottom, menuBounds.bottom)
|
|
||||||
) / 2
|
|
||||||
(intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return TransformOrigin(pivotX, pivotY)
|
|
||||||
}
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.utils.ScrollingInfo
|
||||||
|
import it.vfsfitvnm.vimusic.utils.scrollingInfo
|
||||||
|
import it.vfsfitvnm.vimusic.utils.smoothScrollToTop
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyGridState: LazyGridState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
visible: Boolean = true,
|
||||||
|
iconId: Int? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
windowInsets: WindowInsets = LocalPlayerAwareWindowInsets.current
|
||||||
|
) {
|
||||||
|
val transitionState = remember {
|
||||||
|
MutableTransitionState<ScrollingInfo?>(ScrollingInfo())
|
||||||
|
}.apply { targetState = if (visible) lazyGridState.scrollingInfo() else null }
|
||||||
|
|
||||||
|
FloatingActions(
|
||||||
|
transitionState = transitionState,
|
||||||
|
onScrollToTop = lazyGridState::smoothScrollToTop,
|
||||||
|
iconId = iconId,
|
||||||
|
onClick = onClick,
|
||||||
|
windowInsets = windowInsets,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
visible: Boolean = true,
|
||||||
|
iconId: Int? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
windowInsets: WindowInsets = LocalPlayerAwareWindowInsets.current
|
||||||
|
) {
|
||||||
|
val transitionState = remember {
|
||||||
|
MutableTransitionState<ScrollingInfo?>(ScrollingInfo())
|
||||||
|
}.apply { targetState = if (visible) lazyListState.scrollingInfo() else null }
|
||||||
|
|
||||||
|
FloatingActions(
|
||||||
|
transitionState = transitionState,
|
||||||
|
onScrollToTop = lazyListState::smoothScrollToTop,
|
||||||
|
iconId = iconId,
|
||||||
|
onClick = onClick,
|
||||||
|
windowInsets = windowInsets,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.FloatingActionsContainerWithScrollToTop(
|
||||||
|
scrollState: ScrollState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
visible: Boolean = true,
|
||||||
|
iconId: Int? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
windowInsets: WindowInsets = LocalPlayerAwareWindowInsets.current
|
||||||
|
) {
|
||||||
|
val transitionState = remember {
|
||||||
|
MutableTransitionState<ScrollingInfo?>(ScrollingInfo())
|
||||||
|
}.apply { targetState = if (visible) scrollState.scrollingInfo() else null }
|
||||||
|
|
||||||
|
FloatingActions(
|
||||||
|
transitionState = transitionState,
|
||||||
|
iconId = iconId,
|
||||||
|
onClick = onClick,
|
||||||
|
windowInsets = windowInsets,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.FloatingActions(
|
||||||
|
transitionState: MutableTransitionState<ScrollingInfo?>,
|
||||||
|
windowInsets: WindowInsets,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onScrollToTop: (suspend () -> Unit)? = null,
|
||||||
|
iconId: Int? = null,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val transition = updateTransition(transitionState, "")
|
||||||
|
|
||||||
|
val bottomPaddingValues = windowInsets.only(WindowInsetsSides.Bottom).asPaddingValues()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
modifier = modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.padding(windowInsets.only(WindowInsetsSides.End).asPaddingValues())
|
||||||
|
) {
|
||||||
|
onScrollToTop?.let {
|
||||||
|
transition.AnimatedVisibility(
|
||||||
|
visible = { it?.isScrollingDown == false && it.isFar },
|
||||||
|
enter = slideInVertically(tween(500, if (iconId == null) 0 else 100)) { it },
|
||||||
|
exit = slideOutVertically(tween(500, 0)) { it },
|
||||||
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
SecondaryButton(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
onScrollToTop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = transition.targetState?.isScrollingDown == false && transition.targetState?.isFar == true,
|
||||||
|
iconId = R.drawable.chevron_up,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.padding(bottomPaddingValues)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iconId?.let {
|
||||||
|
onClick?.let {
|
||||||
|
transition.AnimatedVisibility(
|
||||||
|
visible = { it?.isScrollingDown == false },
|
||||||
|
enter = slideInVertically(tween(500, 0)) { it },
|
||||||
|
exit = slideOutVertically(tween(500, 100)) { it },
|
||||||
|
) {
|
||||||
|
PrimaryButton(
|
||||||
|
iconId = iconId,
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = transition.targetState?.isScrollingDown == false,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.padding(bottomPaddingValues)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Header(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actionsContent: @Composable RowScope.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
val typography = LocalAppearance.current.typography
|
||||||
|
|
||||||
|
Header(
|
||||||
|
modifier = modifier,
|
||||||
|
titleContent = {
|
||||||
|
BasicText(
|
||||||
|
text = title,
|
||||||
|
style = typography.xxl.medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actionsContent = actionsContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Header(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
titleContent: @Composable ColumnScope.() -> Unit,
|
||||||
|
actionsContent: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.height(Dimensions.headerHeight)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
titleContent()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp),
|
||||||
|
content = actionsContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HeaderPlaceholder(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.height(128.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.shimmer)
|
||||||
|
.fillMaxWidth(remember { 0.25f + Random.nextFloat() * 0.5f })
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "",
|
||||||
|
style = typography.xxl.medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.Indication
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HeaderIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
@DrawableRes icon: Int,
|
||||||
|
color: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
indication: Indication? = null
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
icon = icon,
|
||||||
|
color = color,
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
indication = indication,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
@DrawableRes icon: Int,
|
||||||
|
color: Color,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
indication: Indication? = null
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(color),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = indication ?: rememberRipple(bounded = false),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.then(modifier)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.isLandscape
|
||||||
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun LayoutWithAdaptiveThumbnail(
|
||||||
|
thumbnailContent: @Composable () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val isLandscape = isLandscape
|
||||||
|
|
||||||
|
if (isLandscape) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
thumbnailContent()
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun adaptiveThumbnailContent(
|
||||||
|
isLoading: Boolean,
|
||||||
|
url: String?,
|
||||||
|
shape: Shape? = null
|
||||||
|
): @Composable () -> Unit = {
|
||||||
|
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
BoxWithConstraints(contentAlignment = Alignment.Center) {
|
||||||
|
val thumbnailSizeDp = if (isLandscape) (maxHeight - 128.dp) else (maxWidth - 64.dp)
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
val modifier = Modifier
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(shape ?: thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Spacer(
|
||||||
|
modifier = modifier
|
||||||
|
.shimmer()
|
||||||
|
.background(colorPalette.shimmer)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = url?.thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components.themed
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import com.valentinilk.shimmer.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoadingOrError(
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null,
|
|
||||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
|
||||||
loadingContent: @Composable ColumnScope.() -> Unit
|
|
||||||
) {
|
|
||||||
Box {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = horizontalAlignment,
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(if (errorMessage == null) 1f else 0f)
|
|
||||||
.shimmer(),
|
|
||||||
content = loadingContent
|
|
||||||
)
|
|
||||||
|
|
||||||
errorMessage?.let {
|
|
||||||
TextCard(
|
|
||||||
icon = R.drawable.alert_circle,
|
|
||||||
onClick = onRetry,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
) {
|
|
||||||
Title(text = onRetry?.let { "Tap to retry" } ?: "Error")
|
|
||||||
Text(text = "An error has occurred:\n$errorMessage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +1,26 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components.themed
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedContentScope
|
import androidx.compose.animation.AnimatedContentScope
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.with
|
import androidx.compose.animation.with
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
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.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
@ -28,13 +33,13 @@ 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
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.layout.onPlaced
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.route.empty
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
@ -42,53 +47,34 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
|
import it.vfsfitvnm.vimusic.models.Song
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.transaction
|
import it.vfsfitvnm.vimusic.transaction
|
||||||
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.viewPlaylistsRoute
|
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.favoritesIcon
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
import it.vfsfitvnm.vimusic.utils.addNext
|
import it.vfsfitvnm.vimusic.utils.addNext
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.shareAsYouTubeSong
|
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun InFavoritesMediaItemMenu(
|
|
||||||
song: DetailedSong,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onDismiss: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
NonQueuedMediaItemMenu(
|
|
||||||
mediaItem = song.asMediaItem,
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
onRemoveFromFavorites = {
|
|
||||||
query {
|
|
||||||
Database.like(song.id, null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun InHistoryMediaItemMenu(
|
fun InHistoryMediaItemMenu(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
song: DetailedSong,
|
song: DetailedSong,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier
|
||||||
onDismiss: (() -> Unit)? = null
|
|
||||||
) {
|
) {
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
var isHiding by remember {
|
var isHiding by remember {
|
||||||
|
@ -100,7 +86,7 @@ fun InHistoryMediaItemMenu(
|
||||||
text = "Do you really hide this song? Its playback time and cache will be wiped.\nThis action is irreversible.",
|
text = "Do you really hide this song? Its playback time and cache will be wiped.\nThis action is irreversible.",
|
||||||
onDismiss = { isHiding = false },
|
onDismiss = { isHiding = false },
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
(onDismiss ?: menuState::hide).invoke()
|
onDismiss()
|
||||||
query {
|
query {
|
||||||
// Not sure we can to this here
|
// Not sure we can to this here
|
||||||
binder?.cache?.removeResource(song.id)
|
binder?.cache?.removeResource(song.id)
|
||||||
|
@ -121,11 +107,11 @@ fun InHistoryMediaItemMenu(
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun InPlaylistMediaItemMenu(
|
fun InPlaylistMediaItemMenu(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
playlistId: Long,
|
playlistId: Long,
|
||||||
positionInPlaylist: Int,
|
positionInPlaylist: Int,
|
||||||
song: DetailedSong,
|
song: DetailedSong,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier
|
||||||
onDismiss: (() -> Unit)? = null
|
|
||||||
) {
|
) {
|
||||||
NonQueuedMediaItemMenu(
|
NonQueuedMediaItemMenu(
|
||||||
mediaItem = song.asMediaItem,
|
mediaItem = song.asMediaItem,
|
||||||
|
@ -143,19 +129,17 @@ fun InPlaylistMediaItemMenu(
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun NonQueuedMediaItemMenu(
|
fun NonQueuedMediaItemMenu(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
mediaItem: MediaItem,
|
mediaItem: MediaItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDismiss: (() -> Unit)? = null,
|
|
||||||
onRemoveFromPlaylist: (() -> Unit)? = null,
|
onRemoveFromPlaylist: (() -> Unit)? = null,
|
||||||
onHideFromDatabase: (() -> Unit)? = null,
|
onHideFromDatabase: (() -> Unit)? = null,
|
||||||
onRemoveFromFavorites: (() -> Unit)? = null,
|
|
||||||
) {
|
) {
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
BaseMediaItemMenu(
|
BaseMediaItemMenu(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
onDismiss = onDismiss ?: menuState::hide,
|
onDismiss = onDismiss,
|
||||||
onStartRadio = {
|
onStartRadio = {
|
||||||
binder?.stopRadio()
|
binder?.stopRadio()
|
||||||
binder?.player?.forcePlay(mediaItem)
|
binder?.player?.forcePlay(mediaItem)
|
||||||
|
@ -170,7 +154,6 @@ fun NonQueuedMediaItemMenu(
|
||||||
onEnqueue = { binder?.player?.enqueue(mediaItem) },
|
onEnqueue = { binder?.player?.enqueue(mediaItem) },
|
||||||
onRemoveFromPlaylist = onRemoveFromPlaylist,
|
onRemoveFromPlaylist = onRemoveFromPlaylist,
|
||||||
onHideFromDatabase = onHideFromDatabase,
|
onHideFromDatabase = onHideFromDatabase,
|
||||||
onRemoveFromFavorites = onRemoveFromFavorites,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -178,22 +161,19 @@ fun NonQueuedMediaItemMenu(
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun QueuedMediaItemMenu(
|
fun QueuedMediaItemMenu(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
mediaItem: MediaItem,
|
mediaItem: MediaItem,
|
||||||
indexInQueue: Int?,
|
indexInQueue: Int?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier
|
||||||
onDismiss: (() -> Unit)? = null,
|
|
||||||
onGlobalRouteEmitted: (() -> Unit)? = null
|
|
||||||
) {
|
) {
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
BaseMediaItemMenu(
|
BaseMediaItemMenu(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
onDismiss = onDismiss ?: menuState::hide,
|
onDismiss = onDismiss,
|
||||||
onRemoveFromQueue = if (indexInQueue != null) ({
|
onRemoveFromQueue = if (indexInQueue != null) ({
|
||||||
binder?.player?.removeMediaItem(indexInQueue)
|
binder?.player?.removeMediaItem(indexInQueue)
|
||||||
}) else null,
|
}) else null,
|
||||||
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -201,19 +181,17 @@ fun QueuedMediaItemMenu(
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseMediaItemMenu(
|
fun BaseMediaItemMenu(
|
||||||
mediaItem: MediaItem,
|
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
mediaItem: MediaItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onGoToEqualizer: (() -> Unit)? = null,
|
onGoToEqualizer: (() -> Unit)? = null,
|
||||||
onSetSleepTimer: (() -> Unit)? = null,
|
onShowSleepTimer: (() -> Unit)? = null,
|
||||||
onStartRadio: (() -> Unit)? = null,
|
onStartRadio: (() -> Unit)? = null,
|
||||||
onPlayNext: (() -> Unit)? = null,
|
onPlayNext: (() -> Unit)? = null,
|
||||||
onEnqueue: (() -> Unit)? = null,
|
onEnqueue: (() -> Unit)? = null,
|
||||||
onRemoveFromQueue: (() -> Unit)? = null,
|
onRemoveFromQueue: (() -> Unit)? = null,
|
||||||
onRemoveFromPlaylist: (() -> Unit)? = null,
|
onRemoveFromPlaylist: (() -> Unit)? = null,
|
||||||
onHideFromDatabase: (() -> Unit)? = null,
|
onHideFromDatabase: (() -> Unit)? = null,
|
||||||
onRemoveFromFavorites: (() -> Unit)? = null,
|
|
||||||
onGlobalRouteEmitted: (() -> Unit)? = null,
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
@ -221,7 +199,7 @@ fun BaseMediaItemMenu(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
onGoToEqualizer = onGoToEqualizer,
|
onGoToEqualizer = onGoToEqualizer,
|
||||||
onSetSleepTimer = onSetSleepTimer,
|
onShowSleepTimer = onShowSleepTimer,
|
||||||
onStartRadio = onStartRadio,
|
onStartRadio = onStartRadio,
|
||||||
onPlayNext = onPlayNext,
|
onPlayNext = onPlayNext,
|
||||||
onEnqueue = onEnqueue,
|
onEnqueue = onEnqueue,
|
||||||
|
@ -238,15 +216,22 @@ fun BaseMediaItemMenu(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHideFromDatabase = onHideFromDatabase,
|
onHideFromDatabase = onHideFromDatabase,
|
||||||
onRemoveFromFavorites = onRemoveFromFavorites,
|
|
||||||
onRemoveFromPlaylist = onRemoveFromPlaylist,
|
onRemoveFromPlaylist = onRemoveFromPlaylist,
|
||||||
onRemoveFromQueue = onRemoveFromQueue,
|
onRemoveFromQueue = onRemoveFromQueue,
|
||||||
onGoToAlbum = albumRoute::global,
|
onGoToAlbum = albumRoute::global,
|
||||||
onGoToArtist = artistRoute::global,
|
onGoToArtist = artistRoute::global,
|
||||||
onShare = {
|
onShare = {
|
||||||
context.shareAsYouTubeSong(mediaItem)
|
val sendIntent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
"https://music.youtube.com/watch?v=${mediaItem.mediaId}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(Intent.createChooser(sendIntent, null))
|
||||||
},
|
},
|
||||||
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -254,39 +239,48 @@ fun BaseMediaItemMenu(
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaItemMenu(
|
fun MediaItemMenu(
|
||||||
mediaItem: MediaItem,
|
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
mediaItem: MediaItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onGoToEqualizer: (() -> Unit)? = null,
|
onGoToEqualizer: (() -> Unit)? = null,
|
||||||
onSetSleepTimer: (() -> Unit)? = null,
|
onShowSleepTimer: (() -> Unit)? = null,
|
||||||
onStartRadio: (() -> Unit)? = null,
|
onStartRadio: (() -> Unit)? = null,
|
||||||
onPlayNext: (() -> Unit)? = null,
|
onPlayNext: (() -> Unit)? = null,
|
||||||
onEnqueue: (() -> Unit)? = null,
|
onEnqueue: (() -> Unit)? = null,
|
||||||
onHideFromDatabase: (() -> Unit)? = null,
|
onHideFromDatabase: (() -> Unit)? = null,
|
||||||
onRemoveFromQueue: (() -> Unit)? = null,
|
onRemoveFromQueue: (() -> Unit)? = null,
|
||||||
onRemoveFromFavorites: (() -> Unit)? = null,
|
|
||||||
onRemoveFromPlaylist: (() -> Unit)? = null,
|
onRemoveFromPlaylist: (() -> Unit)? = null,
|
||||||
onAddToPlaylist: ((Playlist, Int) -> Unit)? = null,
|
onAddToPlaylist: ((Playlist, Int) -> Unit)? = null,
|
||||||
onGoToAlbum: ((String) -> Unit)? = null,
|
onGoToAlbum: ((String) -> Unit)? = null,
|
||||||
onGoToArtist: ((String) -> Unit)? = null,
|
onGoToArtist: ((String) -> Unit)? = null,
|
||||||
onShare: (() -> Unit)? = null,
|
onShare: () -> Unit
|
||||||
onGlobalRouteEmitted: (() -> Unit)? = null,
|
|
||||||
) {
|
) {
|
||||||
Menu(modifier = modifier) {
|
val (colorPalette) = LocalAppearance.current
|
||||||
RouteHandler(
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
var isViewingPlaylists by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var height by remember {
|
||||||
|
mutableStateOf(0.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val likedAt by remember(mediaItem.mediaId) {
|
||||||
|
Database.likedAt(mediaItem.mediaId).distinctUntilChanged()
|
||||||
|
}.collectAsState(initial = null, context = Dispatchers.IO)
|
||||||
|
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = isViewingPlaylists,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
when (targetState.route) {
|
val animationSpec = tween<IntOffset>(400)
|
||||||
viewPlaylistsRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Left) with
|
val slideDirection = if (targetState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right
|
||||||
slideOutOfContainer(AnimatedContentScope.SlideDirection.Left)
|
|
||||||
else -> when (initialState.route) {
|
slideIntoContainer(slideDirection, animationSpec) with
|
||||||
viewPlaylistsRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Right) with
|
slideOutOfContainer(slideDirection, animationSpec)
|
||||||
slideOutOfContainer(AnimatedContentScope.SlideDirection.Right)
|
|
||||||
else -> empty
|
|
||||||
}
|
}
|
||||||
}
|
) { currentIsViewingPlaylists ->
|
||||||
}
|
if (currentIsViewingPlaylists) {
|
||||||
) {
|
|
||||||
viewPlaylistsRoute {
|
|
||||||
val playlistPreviews by remember {
|
val playlistPreviews by remember {
|
||||||
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending)
|
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending)
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
||||||
|
@ -298,9 +292,7 @@ fun MediaItemMenu(
|
||||||
if (isCreatingNewPlaylist && onAddToPlaylist != null) {
|
if (isCreatingNewPlaylist && onAddToPlaylist != null) {
|
||||||
TextFieldDialog(
|
TextFieldDialog(
|
||||||
hintText = "Enter the playlist name",
|
hintText = "Enter the playlist name",
|
||||||
onDismiss = {
|
onDismiss = { isCreatingNewPlaylist = false },
|
||||||
isCreatingNewPlaylist = false
|
|
||||||
},
|
|
||||||
onDone = { text ->
|
onDone = { text ->
|
||||||
onDismiss()
|
onDismiss()
|
||||||
onAddToPlaylist(Playlist(name = text), 0)
|
onAddToPlaylist(Playlist(name = text), 0)
|
||||||
|
@ -308,39 +300,40 @@ fun MediaItemMenu(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
BackHandler {
|
||||||
|
isViewingPlaylists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu(
|
||||||
|
modifier = modifier
|
||||||
|
.requiredHeight(height)
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
MenuBackButton(onClick = pop)
|
IconButton(
|
||||||
|
onClick = { isViewingPlaylists = false },
|
||||||
|
icon = R.drawable.chevron_back,
|
||||||
|
color = colorPalette.textSecondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
if (onAddToPlaylist != null) {
|
if (onAddToPlaylist != null) {
|
||||||
MenuIconButton(
|
SecondaryTextButton(
|
||||||
icon = R.drawable.add,
|
text = "New playlist",
|
||||||
onClick = {
|
onClick = { isCreatingNewPlaylist = true },
|
||||||
isCreatingNewPlaylist = true
|
alternative = true
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddToPlaylist?.let { onAddToPlaylist ->
|
onAddToPlaylist?.let { onAddToPlaylist ->
|
||||||
if (onRemoveFromFavorites == null) {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.heart,
|
|
||||||
text = "Favorites",
|
|
||||||
onClick = {
|
|
||||||
onDismiss()
|
|
||||||
query {
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
Database.like(mediaItem.mediaId, System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
playlistPreviews.forEach { playlistPreview ->
|
playlistPreviews.forEach { playlistPreview ->
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.playlist,
|
icon = R.drawable.playlist,
|
||||||
|
@ -357,15 +350,61 @@ fun MediaItemMenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
Menu(
|
||||||
host {
|
modifier = modifier
|
||||||
Column(
|
.onPlaced { height = with(density) { it.size.height.toDp() } }
|
||||||
modifier = Modifier
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures { }
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
SongItem(
|
||||||
|
song = mediaItem,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
trailingContent = {
|
||||||
|
IconButton(
|
||||||
|
icon = if (likedAt == null) R.drawable.heart_outline else R.drawable.heart,
|
||||||
|
color = colorPalette.favoritesIcon,
|
||||||
|
onClick = {
|
||||||
|
query {
|
||||||
|
if (Database.like(
|
||||||
|
mediaItem.mediaId,
|
||||||
|
if (likedAt == null) System.currentTimeMillis() else null
|
||||||
|
) == 0
|
||||||
|
) {
|
||||||
|
Database.insert(mediaItem, Song::toggleLike)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onShare)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(0.5f)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.background(colorPalette.textDisabled)
|
||||||
|
.height(1.dp)
|
||||||
|
.fillMaxWidth(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
onStartRadio?.let { onStartRadio ->
|
onStartRadio?.let { onStartRadio ->
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.radio,
|
icon = R.drawable.radio,
|
||||||
|
@ -410,15 +449,17 @@ fun MediaItemMenu(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetSleepTimer?.let {
|
// TODO: find solution to this shit
|
||||||
|
onShowSleepTimer?.let {
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (_, typography) = LocalAppearance.current
|
||||||
|
|
||||||
var isShowingSleepTimerDialog by remember {
|
var isShowingSleepTimerDialog by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sleepTimerMillisLeft by (binder?.sleepTimerMillisLeft ?: flowOf(null))
|
val sleepTimerMillisLeft by (binder?.sleepTimerMillisLeft
|
||||||
|
?: flowOf(null))
|
||||||
.collectAsState(initial = null)
|
.collectAsState(initial = null)
|
||||||
|
|
||||||
if (isShowingSleepTimerDialog) {
|
if (isShowingSleepTimerDialog) {
|
||||||
|
@ -429,17 +470,17 @@ fun MediaItemMenu(
|
||||||
confirmText = "Stop",
|
confirmText = "Stop",
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
isShowingSleepTimerDialog = false
|
isShowingSleepTimerDialog = false
|
||||||
|
onDismiss()
|
||||||
},
|
},
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
binder?.cancelSleepTimer()
|
binder?.cancelSleepTimer()
|
||||||
|
onDismiss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
DefaultDialog(
|
DefaultDialog(onDismiss = {
|
||||||
onDismiss = {
|
|
||||||
isShowingSleepTimerDialog = false
|
isShowingSleepTimerDialog = false
|
||||||
}
|
}) {
|
||||||
) {
|
|
||||||
var amount by remember {
|
var amount by remember {
|
||||||
mutableStateOf(1)
|
mutableStateOf(1)
|
||||||
}
|
}
|
||||||
|
@ -509,23 +550,22 @@ fun MediaItemMenu(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
ChunkyButton(
|
DialogTextButton(
|
||||||
backgroundColor = Color.Transparent,
|
|
||||||
text = "Cancel",
|
text = "Cancel",
|
||||||
textStyle = typography.xs.semiBold,
|
onClick = {
|
||||||
shape = RoundedCornerShape(36.dp),
|
isShowingSleepTimerDialog = false
|
||||||
onClick = { isShowingSleepTimerDialog = false }
|
onDismiss()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ChunkyButton(
|
DialogTextButton(
|
||||||
backgroundColor = colorPalette.accent,
|
|
||||||
text = "Set",
|
text = "Set",
|
||||||
textStyle = typography.xs.semiBold.color(colorPalette.onAccent),
|
enabled = amount > 0,
|
||||||
shape = RoundedCornerShape(36.dp),
|
primary = true,
|
||||||
isEnabled = amount > 0,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
binder?.startSleepTimer(amount * 10 * 60 * 1000L)
|
binder?.startSleepTimer(amount * 10 * 60 * 1000L)
|
||||||
isShowingSleepTimerDialog = false
|
isShowingSleepTimerDialog = false
|
||||||
|
onDismiss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -552,9 +592,16 @@ fun MediaItemMenu(
|
||||||
if (onAddToPlaylist != null) {
|
if (onAddToPlaylist != null) {
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.playlist,
|
icon = R.drawable.playlist,
|
||||||
text = "Add to playlist or favorites",
|
text = "Add to playlist",
|
||||||
onClick = {
|
onClick = { isViewingPlaylists = true },
|
||||||
viewPlaylistsRoute()
|
trailingContent = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.chevron_forward),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(colorPalette.textSecondary),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -566,7 +613,6 @@ fun MediaItemMenu(
|
||||||
text = "Go to album",
|
text = "Go to album",
|
||||||
onClick = {
|
onClick = {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
onGlobalRouteEmitted?.invoke()
|
|
||||||
onGoToAlbum(albumId)
|
onGoToAlbum(albumId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -586,7 +632,6 @@ fun MediaItemMenu(
|
||||||
text = "More of $authorName",
|
text = "More of $authorName",
|
||||||
onClick = {
|
onClick = {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
onGlobalRouteEmitted?.invoke()
|
|
||||||
onGoToArtist(authorId)
|
onGoToArtist(authorId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -596,17 +641,6 @@ fun MediaItemMenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onShare?.let { onShare ->
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.share_social,
|
|
||||||
text = "Share",
|
|
||||||
onClick = {
|
|
||||||
onDismiss()
|
|
||||||
onShare()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemoveFromQueue?.let { onRemoveFromQueue ->
|
onRemoveFromQueue?.let { onRemoveFromQueue ->
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.trash,
|
icon = R.drawable.trash,
|
||||||
|
@ -618,17 +652,6 @@ fun MediaItemMenu(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveFromFavorites?.let { onRemoveFromFavorites ->
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.heart_dislike,
|
|
||||||
text = "Remove from favorites",
|
|
||||||
onClick = {
|
|
||||||
onDismiss()
|
|
||||||
onRemoveFromFavorites()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemoveFromPlaylist?.let { onRemoveFromPlaylist ->
|
onRemoveFromPlaylist?.let { onRemoveFromPlaylist ->
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.trash,
|
icon = R.drawable.trash,
|
||||||
|
@ -651,4 +674,3 @@ fun MediaItemMenu(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components.themed
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -17,16 +16,13 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
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 it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
@ -57,7 +53,8 @@ fun MenuEntry(
|
||||||
text: String,
|
text: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
secondaryText: String? = null,
|
secondaryText: String? = null,
|
||||||
isEnabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
@ -65,14 +62,9 @@ fun MenuEntry(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
enabled = isEnabled,
|
|
||||||
onClick = onClick
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.alpha(if (isEnabled) 1f else 0.4f)
|
.alpha(if (enabled) 1f else 0.4f)
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
|
@ -83,7 +75,10 @@ fun MenuEntry(
|
||||||
.size(15.dp)
|
.size(15.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Column {
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = text,
|
text = text,
|
||||||
style = typography.xs.medium
|
style = typography.xs.medium
|
||||||
|
@ -96,41 +91,7 @@ fun MenuEntry(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
trailingContent?.invoke()
|
||||||
fun MenuIconButton(
|
|
||||||
@DrawableRes icon: Int,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = 14.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(icon),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MenuBackButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
MenuIconButton(
|
|
||||||
icon = R.drawable.chevron_back,
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.isLandscape
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun NavigationRail(
|
||||||
|
topIconButtonId: Int,
|
||||||
|
noinline onTopIconButtonClick: () -> Unit,
|
||||||
|
tabIndex: Int,
|
||||||
|
crossinline onTabIndexChanged: (Int) -> Unit,
|
||||||
|
content: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
val isLandscape = isLandscape
|
||||||
|
|
||||||
|
val paddingValues = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.Start).asPaddingValues()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.TopCenter,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(
|
||||||
|
width = if (isLandscape) Dimensions.navigationRailWidthLandscape else Dimensions.navigationRailWidth,
|
||||||
|
height = Dimensions.headerHeight
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(topIconButtonId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(
|
||||||
|
x = if (isLandscape) 0.dp else Dimensions.navigationRailIconOffset,
|
||||||
|
y = 48.dp
|
||||||
|
)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(onClick = onTopIconButtonClick)
|
||||||
|
.padding(all = 12.dp)
|
||||||
|
.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(if (isLandscape) Dimensions.navigationRailWidthLandscape else Dimensions.navigationRailWidth)
|
||||||
|
) {
|
||||||
|
val transition = updateTransition(targetState = tabIndex, label = null)
|
||||||
|
|
||||||
|
content { index, text, icon ->
|
||||||
|
val dothAlpha by transition.animateFloat(label = "") {
|
||||||
|
if (it == index) 1f else 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
val textColor by transition.animateColor(label = "") {
|
||||||
|
if (it == index) colorPalette.text else colorPalette.textDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
val iconContent: @Composable () -> Unit = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.vertical(enabled = !isLandscape)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = dothAlpha
|
||||||
|
translationX = (1f - dothAlpha) * -48.dp.toPx()
|
||||||
|
rotationZ = if (isLandscape) 0f else -90f
|
||||||
|
}
|
||||||
|
.size(Dimensions.navigationRailIconOffset * 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val textContent: @Composable () -> Unit = {
|
||||||
|
BasicText(
|
||||||
|
text = text,
|
||||||
|
style = typography.xs.semiBold.center.color(textColor),
|
||||||
|
modifier = Modifier
|
||||||
|
.vertical(enabled = !isLandscape)
|
||||||
|
.rotate(if (isLandscape) 0f else -90f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentModifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.clickable(onClick = { onTabIndexChanged(index) })
|
||||||
|
|
||||||
|
if (isLandscape) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = contentModifier
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
iconContent()
|
||||||
|
textContent()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = contentModifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
iconContent()
|
||||||
|
textContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.vertical(enabled: Boolean = true) =
|
||||||
|
if (enabled)
|
||||||
|
layout { measurable, constraints ->
|
||||||
|
val placeable = measurable.measure(constraints.copy(maxWidth = Int.MAX_VALUE))
|
||||||
|
layout(placeable.height, placeable.width) {
|
||||||
|
placeable.place(
|
||||||
|
x = -(placeable.width / 2 - placeable.height / 2),
|
||||||
|
y = -(placeable.height / 2 - placeable.width / 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else this
|
|
@ -0,0 +1,44 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrimaryButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
@DrawableRes iconId: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.size(62.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(iconId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedContentScope
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.with
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun Scaffold(
|
||||||
|
topIconButtonId: Int,
|
||||||
|
onTopIconButtonClick: () -> Unit,
|
||||||
|
tabIndex: Int,
|
||||||
|
onTabChanged: (Int) -> Unit,
|
||||||
|
tabColumnContent: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable AnimatedVisibilityScope.(Int) -> Unit
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
NavigationRail(
|
||||||
|
topIconButtonId = topIconButtonId,
|
||||||
|
onTopIconButtonClick = onTopIconButtonClick,
|
||||||
|
tabIndex = tabIndex,
|
||||||
|
onTabIndexChanged = onTabChanged,
|
||||||
|
content = tabColumnContent
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = tabIndex,
|
||||||
|
transitionSpec = {
|
||||||
|
val slideDirection = when (targetState > initialState) {
|
||||||
|
true -> AnimatedContentScope.SlideDirection.Up
|
||||||
|
false -> AnimatedContentScope.SlideDirection.Down
|
||||||
|
}
|
||||||
|
|
||||||
|
val animationSpec = spring(
|
||||||
|
dampingRatio = 0.9f,
|
||||||
|
stiffness = Spring.StiffnessLow,
|
||||||
|
visibilityThreshold = IntOffset.VisibilityThreshold
|
||||||
|
)
|
||||||
|
|
||||||
|
slideIntoContainer(slideDirection, animationSpec) with
|
||||||
|
slideOutOfContainer(slideDirection, animationSpec)
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SecondaryButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
@DrawableRes iconId: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.size(48.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(iconId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SecondaryTextButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
alternative: Boolean = false
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = text,
|
||||||
|
style = typography.xxs.medium,
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
|
.background(if (alternative) colorPalette.background0 else colorPalette.background2)
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,113 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.components.themed
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.utils.align
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TextCard(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
@DrawableRes icon: Int? = null,
|
|
||||||
iconColor: ColorFilter? = null,
|
|
||||||
onClick: (() -> Unit)? = null,
|
|
||||||
content: @Composable TextCardScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
enabled = onClick != null,
|
|
||||||
onClick = onClick ?: {}
|
|
||||||
)
|
|
||||||
.background(colorPalette.background1)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
|
||||||
) {
|
|
||||||
icon?.let {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(icon),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = iconColor ?: ColorFilter.tint(Color.Red),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
(icon?.let { IconTextCardScopeImpl } ?: TextCardScopeImpl).content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextCardScope {
|
|
||||||
@Composable
|
|
||||||
fun Title(text: String)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Text(text: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
private object TextCardScopeImpl : TextCardScope {
|
|
||||||
@Composable
|
|
||||||
override fun Title(text: String) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xxs.semiBold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Text(text: String) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xxs.secondary.align(TextAlign.Justify),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private object IconTextCardScopeImpl : TextCardScope {
|
|
||||||
@Composable
|
|
||||||
override fun Title(text: String) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xxs.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Text(text: String) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xxs.secondary,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
155
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/AlbumItem.kt
Normal file
155
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/AlbumItem.kt
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.items
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlbumItem(
|
||||||
|
album: Album,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false
|
||||||
|
) {
|
||||||
|
AlbumItem(
|
||||||
|
thumbnailUrl = album.thumbnailUrl,
|
||||||
|
title = album.title,
|
||||||
|
authors = album.authorsText,
|
||||||
|
year = album.year,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
alternative = alternative,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlbumItem(
|
||||||
|
album: Innertube.AlbumItem,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false
|
||||||
|
) {
|
||||||
|
AlbumItem(
|
||||||
|
thumbnailUrl = album.thumbnail?.url,
|
||||||
|
title = album.info?.name,
|
||||||
|
authors = album.authors?.joinToString("") { it.name ?: "" },
|
||||||
|
year = album.year,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
alternative = alternative,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlbumItem(
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
title: String?,
|
||||||
|
authors: String?,
|
||||||
|
year: String?,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false
|
||||||
|
) {
|
||||||
|
val (_, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = alternative,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemInfoContainer {
|
||||||
|
BasicText(
|
||||||
|
text = title ?: "",
|
||||||
|
style = typography.xs.semiBold,
|
||||||
|
maxLines = if (alternative) 1 else 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!alternative) {
|
||||||
|
authors?.let {
|
||||||
|
BasicText(
|
||||||
|
text = authors,
|
||||||
|
style = typography.xs.semiBold.secondary,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = year ?: "",
|
||||||
|
style = typography.xxs.semiBold.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AlbumItemPlaceholder(
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false
|
||||||
|
) {
|
||||||
|
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = alternative,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemInfoContainer {
|
||||||
|
TextPlaceholder()
|
||||||
|
|
||||||
|
if (!alternative) {
|
||||||
|
TextPlaceholder()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
145
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ArtistItem.kt
Normal file
145
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ArtistItem.kt
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.items
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ArtistItem(
|
||||||
|
artist: Artist,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
ArtistItem(
|
||||||
|
thumbnailUrl = artist.thumbnailUrl,
|
||||||
|
name = artist.name,
|
||||||
|
subscribersCount = null,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier,
|
||||||
|
alternative = alternative
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ArtistItem(
|
||||||
|
artist: Innertube.ArtistItem,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
ArtistItem(
|
||||||
|
thumbnailUrl = artist.thumbnail?.url,
|
||||||
|
name = artist.info?.name,
|
||||||
|
subscribersCount = artist.subscribersCountText,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier,
|
||||||
|
alternative = alternative
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ArtistItem(
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
name: String?,
|
||||||
|
subscribersCount: String?,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
val (_, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = alternative,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.requiredSize(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemInfoContainer(
|
||||||
|
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = name ?: "",
|
||||||
|
style = typography.xs.semiBold,
|
||||||
|
maxLines = if (alternative) 1 else 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribersCount?.let {
|
||||||
|
BasicText(
|
||||||
|
text = subscribersCount,
|
||||||
|
style = typography.xxs.semiBold.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ArtistItemPlaceholder(
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = alternative,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = CircleShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemInfoContainer(
|
||||||
|
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
|
||||||
|
) {
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.items
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun ItemContainer(
|
||||||
|
alternative: Boolean,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||||
|
content: @Composable (centeredModifier: Modifier) -> Unit
|
||||||
|
) {
|
||||||
|
if (alternative) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||||
|
.width(thumbnailSizeDp)
|
||||||
|
) {
|
||||||
|
content(
|
||||||
|
centeredModifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = verticalAlignment,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
content(
|
||||||
|
centeredModifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun ItemInfoContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,274 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.items
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.overlay
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistItem(
|
||||||
|
@DrawableRes icon: Int,
|
||||||
|
colorTint: Color,
|
||||||
|
name: String?,
|
||||||
|
songCount: Int?,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
PlaylistItem(
|
||||||
|
thumbnailContent = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorTint),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
songCount = songCount,
|
||||||
|
name = name,
|
||||||
|
channelName = null,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier,
|
||||||
|
alternative = alternative
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistItem(
|
||||||
|
playlist: PlaylistPreview,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
val thumbnails by remember {
|
||||||
|
Database.playlistThumbnailUrls(playlist.playlist.id).distinctUntilChanged().map {
|
||||||
|
it.map { url ->
|
||||||
|
url.thumbnail(thumbnailSizePx / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
||||||
|
|
||||||
|
PlaylistItem(
|
||||||
|
thumbnailContent = {
|
||||||
|
if (thumbnails.toSet().size == 1) {
|
||||||
|
AsyncImage(
|
||||||
|
model = thumbnails.first().thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = it
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = it
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
Alignment.TopStart,
|
||||||
|
Alignment.TopEnd,
|
||||||
|
Alignment.BottomStart,
|
||||||
|
Alignment.BottomEnd
|
||||||
|
).forEachIndexed { index, alignment ->
|
||||||
|
AsyncImage(
|
||||||
|
model = thumbnails.getOrNull(index),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(alignment)
|
||||||
|
.size(thumbnailSizeDp / 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
songCount = playlist.songCount,
|
||||||
|
name = playlist.playlist.name,
|
||||||
|
channelName = null,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier,
|
||||||
|
alternative = alternative
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistItem(
|
||||||
|
playlist: Innertube.PlaylistItem,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
PlaylistItem(
|
||||||
|
thumbnailUrl = playlist.thumbnail?.url,
|
||||||
|
songCount = playlist.songCount,
|
||||||
|
name = playlist.info?.name,
|
||||||
|
channelName = playlist.channel?.name,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier,
|
||||||
|
alternative = alternative
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistItem(
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
songCount: Int?,
|
||||||
|
name: String?,
|
||||||
|
channelName: String?,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
PlaylistItem(
|
||||||
|
thumbnailContent = {
|
||||||
|
AsyncImage(
|
||||||
|
model = thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = it
|
||||||
|
)
|
||||||
|
},
|
||||||
|
songCount = songCount,
|
||||||
|
name = name,
|
||||||
|
channelName = channelName,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier,
|
||||||
|
alternative = alternative,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistItem(
|
||||||
|
thumbnailContent: @Composable BoxScope.(modifier: Modifier) -> Unit,
|
||||||
|
songCount: Int?,
|
||||||
|
name: String?,
|
||||||
|
channelName: String?,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = alternative,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier
|
||||||
|
) { centeredModifier ->
|
||||||
|
Box(
|
||||||
|
modifier = centeredModifier
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.background(color = colorPalette.background1)
|
||||||
|
.requiredSize(thumbnailSizeDp)
|
||||||
|
) {
|
||||||
|
thumbnailContent(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
songCount?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "$songCount",
|
||||||
|
style = typography.xxs.medium.color(colorPalette.onOverlay),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
|
||||||
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemInfoContainer(
|
||||||
|
horizontalAlignment = if (alternative && channelName == null) Alignment.CenterHorizontally else Alignment.Start,
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = name ?: "",
|
||||||
|
style = typography.xs.semiBold,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
channelName?.let {
|
||||||
|
BasicText(
|
||||||
|
text = channelName,
|
||||||
|
style = typography.xs.semiBold.secondary,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaylistItemPlaceholder(
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alternative: Boolean = false,
|
||||||
|
) {
|
||||||
|
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = alternative,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemInfoContainer(
|
||||||
|
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
|
||||||
|
) {
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
218
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt
Normal file
218
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.items
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SongItem(
|
||||||
|
song: Innertube.SongItem,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
SongItem(
|
||||||
|
thumbnailUrl = song.thumbnail?.size(thumbnailSizePx),
|
||||||
|
title = song.info?.name,
|
||||||
|
authors = song.authors?.joinToString("") { it.name ?: "" },
|
||||||
|
duration = song.durationText,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SongItem(
|
||||||
|
song: MediaItem,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
SongItem(
|
||||||
|
thumbnailUrl = song.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx)?.toString(),
|
||||||
|
title = song.mediaMetadata.title.toString(),
|
||||||
|
authors = song.mediaMetadata.artist.toString(),
|
||||||
|
duration = song.mediaMetadata.extras?.getString("durationText"),
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
onThumbnailContent = onThumbnailContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SongItem(
|
||||||
|
song: DetailedSong,
|
||||||
|
thumbnailSizePx: Int,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
SongItem(
|
||||||
|
thumbnailUrl = song.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||||
|
title = song.title,
|
||||||
|
authors = song.artistsText,
|
||||||
|
duration = song.durationText,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
onThumbnailContent = onThumbnailContent,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SongItem(
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
title: String?,
|
||||||
|
authors: String?,
|
||||||
|
duration: String?,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null
|
||||||
|
) {
|
||||||
|
SongItem(
|
||||||
|
title = title,
|
||||||
|
authors = authors,
|
||||||
|
duration = duration,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
thumbnailContent = {
|
||||||
|
AsyncImage(
|
||||||
|
model = thumbnailUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(LocalAppearance.current.thumbnailShape)
|
||||||
|
.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
onThumbnailContent?.invoke(this)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
trailingContent = trailingContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SongItem(
|
||||||
|
thumbnailContent: @Composable BoxScope.() -> Unit,
|
||||||
|
title: String?,
|
||||||
|
authors: String?,
|
||||||
|
duration: String?,
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val (_, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = false,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
) {
|
||||||
|
thumbnailContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemInfoContainer {
|
||||||
|
trailingContent?.let {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
BasicText(
|
||||||
|
text = title ?: "",
|
||||||
|
style = typography.xs.semiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
} ?: BasicText(
|
||||||
|
text = title ?: "",
|
||||||
|
style = typography.xs.semiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
BasicText(
|
||||||
|
text = authors ?: "",
|
||||||
|
style = typography.xs.semiBold.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Clip,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
duration?.let {
|
||||||
|
BasicText(
|
||||||
|
text = duration,
|
||||||
|
style = typography.xxs.secondary.medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SongItemPlaceholder(
|
||||||
|
thumbnailSizeDp: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = false,
|
||||||
|
thumbnailSizeDp =thumbnailSizeDp,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemInfoContainer {
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
149
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/VideoItem.kt
Normal file
149
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/VideoItem.kt
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.items
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.overlay
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoItem(
|
||||||
|
video: Innertube.VideoItem,
|
||||||
|
thumbnailHeightDp: Dp,
|
||||||
|
thumbnailWidthDp: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
VideoItem(
|
||||||
|
thumbnailUrl = video.thumbnail?.url,
|
||||||
|
duration = video.durationText,
|
||||||
|
title = video.info?.name,
|
||||||
|
uploader = video.authors?.joinToString("") { it.name ?: "" },
|
||||||
|
views = video.viewsText,
|
||||||
|
thumbnailHeightDp = thumbnailHeightDp,
|
||||||
|
thumbnailWidthDp = thumbnailWidthDp,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoItem(
|
||||||
|
thumbnailUrl: String?,
|
||||||
|
duration: String?,
|
||||||
|
title: String?,
|
||||||
|
uploader: String?,
|
||||||
|
views: String?,
|
||||||
|
thumbnailHeightDp: Dp,
|
||||||
|
thumbnailWidthDp: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = false,
|
||||||
|
thumbnailSizeDp = 0.dp,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
AsyncImage(
|
||||||
|
model = thumbnailUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
duration?.let {
|
||||||
|
BasicText(
|
||||||
|
text = duration,
|
||||||
|
style = typography.xxs.medium.color(colorPalette.onOverlay),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
|
||||||
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemInfoContainer {
|
||||||
|
BasicText(
|
||||||
|
text = title ?: "",
|
||||||
|
style = typography.xs.semiBold,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = uploader ?: "",
|
||||||
|
style = typography.xs.semiBold.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
views?.let {
|
||||||
|
BasicText(
|
||||||
|
text = views,
|
||||||
|
style = typography.xxs.medium.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VideoItemPlaceholder(
|
||||||
|
thumbnailHeightDp: Dp,
|
||||||
|
thumbnailWidthDp: Dp,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
|
ItemContainer(
|
||||||
|
alternative = false,
|
||||||
|
thumbnailSizeDp = 0.dp,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
|
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ItemInfoContainer {
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,421 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.models.Album
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.bold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun AlbumScreen(browseId: String) {
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
val albumResult by remember(browseId) {
|
|
||||||
Database.album(browseId).map { album ->
|
|
||||||
album
|
|
||||||
?.takeIf { album.timestamp != null }
|
|
||||||
?.let(Result.Companion::success)
|
|
||||||
?: YouTube.album(browseId)?.map { youtubeAlbum ->
|
|
||||||
Database.upsert(
|
|
||||||
Album(
|
|
||||||
id = browseId,
|
|
||||||
title = youtubeAlbum.title,
|
|
||||||
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
|
||||||
year = youtubeAlbum.year,
|
|
||||||
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
|
|
||||||
shareUrl = youtubeAlbum.url,
|
|
||||||
timestamp = System.currentTimeMillis()
|
|
||||||
),
|
|
||||||
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
|
|
||||||
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
SongAlbumMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
albumId = browseId,
|
|
||||||
position = position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.distinctUntilChanged()
|
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
|
||||||
|
|
||||||
val songs by remember(browseId) {
|
|
||||||
Database.albumSongs(browseId)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
albumResult?.getOrNull()?.let { album ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = album.thumbnailUrl?.thumbnail(Dimensions.thumbnails.album.px),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.album)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = album.title ?: "Unknown",
|
|
||||||
style = typography.m.semiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = album.authorsText ?: "",
|
|
||||||
style = typography.xs.secondary.semiBold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
|
|
||||||
album.year?.let { year ->
|
|
||||||
BasicText(
|
|
||||||
text = year,
|
|
||||||
style = typography.xs.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: albumResult?.exceptionOrNull()?.let { throwable ->
|
|
||||||
LoadingOrError(errorMessage = throwable.javaClass.canonicalName)
|
|
||||||
} ?: LoadingOrError()
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = songs.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
songs
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ellipsis_horizontal),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
menuState.display {
|
|
||||||
Menu {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.enqueue,
|
|
||||||
text = "Enqueue",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
binder?.player?.enqueue(
|
|
||||||
songs.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.playlist,
|
|
||||||
text = "Import as playlist",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
albumResult
|
|
||||||
?.getOrNull()
|
|
||||||
?.let { album ->
|
|
||||||
query {
|
|
||||||
val playlistId =
|
|
||||||
Database.insert(
|
|
||||||
Playlist(
|
|
||||||
name = album.title
|
|
||||||
?: "Unknown"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
songs.forEachIndexed { index, song ->
|
|
||||||
Database.insert(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = song.id,
|
|
||||||
playlistId = playlistId,
|
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.share_social,
|
|
||||||
text = "Share",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
albumResult?.getOrNull()?.shareUrl?.let { url ->
|
|
||||||
val sendIntent = Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
type = "text/plain"
|
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
sendIntent,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.download,
|
|
||||||
text = "Refetch",
|
|
||||||
secondaryText = albumResult?.getOrNull()?.timestamp?.let { timestamp ->
|
|
||||||
"Last updated on ${
|
|
||||||
DateFormat
|
|
||||||
.getDateTimeInstance()
|
|
||||||
.format(Date(timestamp))
|
|
||||||
}"
|
|
||||||
},
|
|
||||||
isEnabled = albumResult?.getOrNull() != null,
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
query {
|
|
||||||
albumResult
|
|
||||||
?.getOrNull()
|
|
||||||
?.let(Database::delete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = songs,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song }
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
title = song.title,
|
|
||||||
authors = song.artistsText ?: albumResult?.getOrNull()?.authorsText,
|
|
||||||
durationText = song.durationText,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
songs.map(DetailedSong::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
startContent = {
|
|
||||||
BasicText(
|
|
||||||
text = "${index + 1}",
|
|
||||||
style = typography.xs.secondary.bold.center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier
|
|
||||||
.width(36.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
NonQueuedMediaItemMenu(
|
|
||||||
mediaItem = song.asMediaItem,
|
|
||||||
onDismiss = menuState::hide,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoadingOrError(
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = errorMessage,
|
|
||||||
onRetry = onRetry
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = colorPalette.shimmer, shape = ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.album)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
TextPlaceholder()
|
|
||||||
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,379 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.geometry.center
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.models.Artist
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun ArtistScreen(browseId: String) {
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
val artistResult by remember(browseId) {
|
|
||||||
Database.artist(browseId).map { artist ->
|
|
||||||
artist
|
|
||||||
?.takeIf { artist.timestamp != null }
|
|
||||||
?.let(Result.Companion::success)
|
|
||||||
?: fetchArtist(browseId)
|
|
||||||
}.distinctUntilChanged()
|
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
|
||||||
|
|
||||||
val songThumbnailSizePx = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
val songs by remember(browseId) {
|
|
||||||
Database.artistSongs(browseId)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
artistResult?.getOrNull()?.let { artist ->
|
|
||||||
AsyncImage(
|
|
||||||
model = artist.thumbnailUrl?.thumbnail(Dimensions.thumbnails.artist.px),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.size(Dimensions.thumbnails.artist)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = artist.name,
|
|
||||||
style = typography.l.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(32.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
binder?.playRadio(
|
|
||||||
NavigationEndpoint.Endpoint.Watch(
|
|
||||||
videoId = artist.shuffleVideoId,
|
|
||||||
playlistId = artist.shufflePlaylistId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
query {
|
|
||||||
runBlocking {
|
|
||||||
fetchArtist(browseId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.radio),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
binder?.playRadio(
|
|
||||||
NavigationEndpoint.Endpoint.Watch(
|
|
||||||
videoId = artist.radioVideoId
|
|
||||||
?: artist.shuffleVideoId,
|
|
||||||
playlistId = artist.radioPlaylistId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
query {
|
|
||||||
runBlocking {
|
|
||||||
fetchArtist(browseId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: artistResult?.exceptionOrNull()?.let { throwable ->
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = throwable.javaClass.canonicalName,
|
|
||||||
onRetry = {
|
|
||||||
query {
|
|
||||||
runBlocking {
|
|
||||||
Database.artist(browseId).first()?.let(Database::update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} ?: LoadingOrError()
|
|
||||||
}
|
|
||||||
|
|
||||||
item("songs") {
|
|
||||||
if (songs.isEmpty()) return@item
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.zIndex(1f)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 32.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Local tracks",
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = songs.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
songs
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = songs,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song },
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
song = song,
|
|
||||||
thumbnailSize = songThumbnailSizePx,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
songs.map(DetailedSong::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
InHistoryMediaItemMenu(song = song)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
artistResult?.getOrNull()?.info?.let { description ->
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 32.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Information",
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Canvas(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.width(48.dp)
|
|
||||||
) {
|
|
||||||
drawLine(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
start = size.center.copy(y = 0f),
|
|
||||||
end = size.center.copy(y = size.height),
|
|
||||||
strokeWidth = 2.dp.toPx()
|
|
||||||
)
|
|
||||||
|
|
||||||
drawCircle(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
center = size.center.copy(y = size.height),
|
|
||||||
radius = 4.dp.toPx()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = description,
|
|
||||||
style = typography.xxs.secondary.medium.copy(
|
|
||||||
lineHeight = 24.sp,
|
|
||||||
textAlign = TextAlign.Justify
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoadingOrError(
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = errorMessage,
|
|
||||||
onRetry = onRetry,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = colorPalette.shimmer, shape = CircleShape)
|
|
||||||
.size(Dimensions.thumbnails.artist)
|
|
||||||
)
|
|
||||||
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.9f)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
repeat(3) {
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.8f)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchArtist(browseId: String): Result<Artist>? {
|
|
||||||
return YouTube.artist(browseId)
|
|
||||||
?.map { youtubeArtist ->
|
|
||||||
Artist(
|
|
||||||
id = browseId,
|
|
||||||
name = youtubeArtist.name,
|
|
||||||
thumbnailUrl = youtubeArtist.thumbnail?.url,
|
|
||||||
info = youtubeArtist.description,
|
|
||||||
shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,
|
|
||||||
shufflePlaylistId = youtubeArtist.shuffleEndpoint?.playlistId,
|
|
||||||
radioVideoId = youtubeArtist.radioEndpoint?.videoId,
|
|
||||||
radioPlaylistId = youtubeArtist.radioEndpoint?.playlistId,
|
|
||||||
timestamp = System.currentTimeMillis()
|
|
||||||
).also(Database::upsert)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,207 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun BuiltInPlaylistScreen(builtInPlaylist: BuiltInPlaylist) {
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
val songs by remember(binder?.cache, builtInPlaylist) {
|
|
||||||
when (builtInPlaylist) {
|
|
||||||
BuiltInPlaylist.Favorites -> Database.favorites()
|
|
||||||
BuiltInPlaylist.Offline -> Database.songsWithContentLength().map { songs ->
|
|
||||||
songs.filter { song ->
|
|
||||||
song.contentLength?.let {
|
|
||||||
binder?.cache?.isCached(song.id, 0, song.contentLength)
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 16.dp, bottom = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = when (builtInPlaylist) {
|
|
||||||
BuiltInPlaylist.Favorites -> "Favorites"
|
|
||||||
BuiltInPlaylist.Offline -> "Offline"
|
|
||||||
},
|
|
||||||
style = typography.m.semiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = "${songs.size} songs",
|
|
||||||
style = typography.xxs.semiBold.secondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = songs.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
songs
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ellipsis_horizontal),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
menuState.display {
|
|
||||||
Menu {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.enqueue,
|
|
||||||
text = "Enqueue",
|
|
||||||
isEnabled = songs.isNotEmpty(),
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = songs,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song },
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
song = song,
|
|
||||||
thumbnailSize = thumbnailSize,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
songs.map(DetailedSong::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
when (builtInPlaylist) {
|
|
||||||
BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu(song = song)
|
|
||||||
BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(song = song)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,563 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
|
||||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
|
||||||
import it.vfsfitvnm.vimusic.models.SearchQuery
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.badge
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownSection
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownSectionSpacer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownTextItem
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropdownMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.BuiltInPlaylistItem
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.isFirstLaunchKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistGridExpandedKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun HomeScreen() {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
val lazyHorizontalGridState = rememberLazyGridState()
|
|
||||||
|
|
||||||
var playlistSortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded)
|
|
||||||
var playlistSortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending)
|
|
||||||
var playlistGridExpanded by rememberPreference(playlistGridExpandedKey, false)
|
|
||||||
|
|
||||||
val playlistPreviews by remember(playlistSortBy, playlistSortOrder) {
|
|
||||||
Database.playlistPreviews(playlistSortBy, playlistSortOrder)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
var songSortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
|
|
||||||
var songSortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
|
|
||||||
|
|
||||||
val songCollection by remember(songSortBy, songSortOrder) {
|
|
||||||
Database.songs(songSortBy, songSortOrder)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
settingsRoute {
|
|
||||||
SettingsScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
localPlaylistRoute { playlistId ->
|
|
||||||
LocalPlaylistScreen(
|
|
||||||
playlistId = playlistId ?: error("playlistId cannot be null")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
builtInPlaylistRoute { builtInPlaylist ->
|
|
||||||
BuiltInPlaylistScreen(
|
|
||||||
builtInPlaylist = builtInPlaylist
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResultRoute { query ->
|
|
||||||
SearchResultScreen(
|
|
||||||
query = query,
|
|
||||||
onSearchAgain = {
|
|
||||||
searchRoute(query)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchRoute { initialTextInput ->
|
|
||||||
SearchScreen(
|
|
||||||
initialTextInput = initialTextInput,
|
|
||||||
onSearch = { query ->
|
|
||||||
searchResultRoute(query)
|
|
||||||
|
|
||||||
query {
|
|
||||||
Database.insert(SearchQuery(query = query))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUri = { uri ->
|
|
||||||
intentUriRoute(uri)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
albumRoute { browseId ->
|
|
||||||
AlbumScreen(browseId = browseId ?: error("browseId cannot be null"))
|
|
||||||
}
|
|
||||||
|
|
||||||
artistRoute { browseId ->
|
|
||||||
ArtistScreen(
|
|
||||||
browseId = browseId ?: error("browseId cannot be null")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
intentUriRoute { uri ->
|
|
||||||
IntentUriScreen(
|
|
||||||
uri = uri ?: Uri.EMPTY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
host {
|
|
||||||
// This somehow prevents items to not be displayed sometimes...
|
|
||||||
@Suppress("UNUSED_EXPRESSION") playlistPreviews
|
|
||||||
@Suppress("UNUSED_EXPRESSION") songCollection
|
|
||||||
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val isFirstLaunch by rememberPreference(isFirstLaunchKey, true)
|
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
var isCreatingANewPlaylist by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCreatingANewPlaylist) {
|
|
||||||
TextFieldDialog(
|
|
||||||
hintText = "Enter the playlist name",
|
|
||||||
onDismiss = {
|
|
||||||
isCreatingANewPlaylist = false
|
|
||||||
},
|
|
||||||
onDone = { text ->
|
|
||||||
query {
|
|
||||||
Database.insert(Playlist(name = text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item("topAppBar") {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.equalizer),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { settingsRoute() }
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.badge(color = colorPalette.red, isDisplayed = isFirstLaunch)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.search),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { searchRoute("") }
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item("playlistsHeader") {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Your playlists",
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.add),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { isCreatingANewPlaylist = true }
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Box {
|
|
||||||
var isSortMenuDisplayed by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.sort),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { isSortMenuDisplayed = true }
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
isDisplayed = isSortMenuDisplayed,
|
|
||||||
onDismissRequest = { isSortMenuDisplayed = false }
|
|
||||||
) {
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "NAME",
|
|
||||||
isSelected = playlistSortBy == PlaylistSortBy.Name,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortBy = PlaylistSortBy.Name
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "DATE ADDED",
|
|
||||||
isSelected = playlistSortBy == PlaylistSortBy.DateAdded,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortBy = PlaylistSortBy.DateAdded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "SONG COUNT",
|
|
||||||
isSelected = playlistSortBy == PlaylistSortBy.SongCount,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortBy = PlaylistSortBy.SongCount
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDownSectionSpacer()
|
|
||||||
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = when (playlistSortOrder) {
|
|
||||||
SortOrder.Ascending -> "ASCENDING"
|
|
||||||
SortOrder.Descending -> "DESCENDING"
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortOrder = !playlistSortOrder
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropDownSectionSpacer()
|
|
||||||
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = when (playlistGridExpanded) {
|
|
||||||
true -> "COLLAPSE"
|
|
||||||
false -> "EXPAND"
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistGridExpanded = !playlistGridExpanded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item("playlists") {
|
|
||||||
LazyHorizontalGrid(
|
|
||||||
state = lazyHorizontalGridState,
|
|
||||||
rows = GridCells.Fixed(if (playlistGridExpanded) 3 else 1),
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.animateContentSize()
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(124.dp * (if (playlistGridExpanded) 3 else 1))
|
|
||||||
) {
|
|
||||||
item(key = "favorites") {
|
|
||||||
BuiltInPlaylistItem(
|
|
||||||
icon = R.drawable.heart,
|
|
||||||
colorTint = colorPalette.red,
|
|
||||||
name = "Favorites",
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { builtInPlaylistRoute(BuiltInPlaylist.Favorites) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item(key = "offline") {
|
|
||||||
BuiltInPlaylistItem(
|
|
||||||
icon = R.drawable.airplane,
|
|
||||||
colorTint = colorPalette.blue,
|
|
||||||
name = "Offline",
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { builtInPlaylistRoute(BuiltInPlaylist.Offline) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = playlistPreviews,
|
|
||||||
key = { it.playlist.id },
|
|
||||||
contentType = { it }
|
|
||||||
) { playlistPreview ->
|
|
||||||
PlaylistPreviewItem(
|
|
||||||
playlistPreview = playlistPreview,
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { localPlaylistRoute(playlistPreview.playlist.id) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item("songs") {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 32.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Songs",
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = songCollection.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
songCollection
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Box {
|
|
||||||
var isSortMenuDisplayed by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.sort),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
isSortMenuDisplayed = true
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
isDisplayed = isSortMenuDisplayed,
|
|
||||||
onDismissRequest = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "PLAY TIME",
|
|
||||||
isSelected = songSortBy == SongSortBy.PlayTime,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortBy = SongSortBy.PlayTime
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "TITLE",
|
|
||||||
isSelected = songSortBy == SongSortBy.Title,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortBy = SongSortBy.Title
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "DATE ADDED",
|
|
||||||
isSelected = songSortBy == SongSortBy.DateAdded,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortBy = SongSortBy.DateAdded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDownSectionSpacer()
|
|
||||||
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = when (songSortOrder) {
|
|
||||||
SortOrder.Ascending -> "ASCENDING"
|
|
||||||
SortOrder.Descending -> "DESCENDING"
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortOrder = !songSortOrder
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = songCollection,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song }
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
song = song,
|
|
||||||
thumbnailSize = thumbnailSize,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
songCollection.map(DetailedSong::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
InHistoryMediaItemMenu(song = song)
|
|
||||||
},
|
|
||||||
onThumbnailContent = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = songSortBy == SongSortBy.PlayTime,
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut(),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = song.formattedTotalPlayTime,
|
|
||||||
style = typography.xxs.semiBold.center.color(Color.White),
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
brush = Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Black.copy(alpha = 0.75f)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = ThumbnailRoundness.shape
|
|
||||||
)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,269 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
|
||||||
import it.vfsfitvnm.vimusic.transaction
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun IntentUriScreen(uri: Uri) {
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
var itemsResult by remember(uri) {
|
|
||||||
mutableStateOf<Result<List<YouTube.Item.Song>>?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
var playlistBrowseId by rememberSaveable {
|
|
||||||
mutableStateOf<String?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val onLoad = relaunchableEffect(uri) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
itemsResult = uri.getQueryParameter("list")?.let { playlistId ->
|
|
||||||
if (playlistId.startsWith("OLAK5uy_")) {
|
|
||||||
YouTube.queue(playlistId)?.map { songList ->
|
|
||||||
songList ?: emptyList()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
playlistBrowseId = "VL$playlistId"
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} ?: uri.getQueryParameter("v")?.let { videoId ->
|
|
||||||
YouTube.song(videoId)?.map { song ->
|
|
||||||
song?.let { listOf(song) } ?: emptyList()
|
|
||||||
}
|
|
||||||
} ?: uri.takeIf {
|
|
||||||
uri.host == "youtu.be"
|
|
||||||
}?.path?.drop(1)?.let { videoId ->
|
|
||||||
YouTube.song(videoId)?.map { song ->
|
|
||||||
song?.let { listOf(song) } ?: emptyList()
|
|
||||||
}
|
|
||||||
} ?: Result.failure(Error("Missing URL parameters"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playlistBrowseId?.let { browseId ->
|
|
||||||
PlaylistScreen(browseId = browseId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val thumbnailSizePx = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
var isImportingAsPlaylist by remember(uri) {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (isImportingAsPlaylist) {
|
|
||||||
TextFieldDialog(
|
|
||||||
hintText = "Enter the playlist name",
|
|
||||||
onDismiss = {
|
|
||||||
isImportingAsPlaylist = false
|
|
||||||
},
|
|
||||||
onDone = { text ->
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
val playlistId = Database.insert(Playlist(name = text))
|
|
||||||
|
|
||||||
itemsResult
|
|
||||||
?.getOrNull()
|
|
||||||
?.map(YouTube.Item.Song::asMediaItem)
|
|
||||||
?.forEachIndexed { index, mediaItem ->
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
|
|
||||||
Database.insert(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
playlistId = playlistId,
|
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ellipsis_horizontal),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
menuState.display {
|
|
||||||
Menu {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.enqueue,
|
|
||||||
text = "Enqueue",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
itemsResult
|
|
||||||
?.getOrNull()
|
|
||||||
?.map(YouTube.Item.Song::asMediaItem)
|
|
||||||
?.let { mediaItems ->
|
|
||||||
binder?.player?.enqueue(
|
|
||||||
mediaItems
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.playlist,
|
|
||||||
text = "Import as playlist",
|
|
||||||
onClick = {
|
|
||||||
isImportingAsPlaylist = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsResult?.getOrNull()?.let { items ->
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
item {
|
|
||||||
TextCard(icon = R.drawable.sad) {
|
|
||||||
Title(text = "No songs found")
|
|
||||||
Text(text = "Please try a different query or category.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
itemsIndexed(
|
|
||||||
items = items,
|
|
||||||
contentType = { _, item -> item }
|
|
||||||
) { index, item ->
|
|
||||||
SmallSongItem(
|
|
||||||
song = item,
|
|
||||||
thumbnailSizePx = thumbnailSizePx,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
items.map(YouTube.Item.Song::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: itemsResult?.exceptionOrNull()?.let { throwable ->
|
|
||||||
item {
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = throwable.javaClass.canonicalName,
|
|
||||||
onRetry = onLoad
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: item {
|
|
||||||
LoadingOrError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoadingOrError(
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = errorMessage,
|
|
||||||
onRetry = onRetry
|
|
||||||
) {
|
|
||||||
repeat(5) { index ->
|
|
||||||
SmallSongItemShimmer(
|
|
||||||
thumbnailSizeDp = Dimensions.thumbnails.song,
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(1f - index * 0.175f)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 4.dp, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,333 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
|
||||||
import it.vfsfitvnm.reordering.animateItemPlacement
|
|
||||||
import it.vfsfitvnm.reordering.draggedItem
|
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
|
||||||
import it.vfsfitvnm.reordering.reorder
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.transaction
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun LocalPlaylistScreen(playlistId: Long) {
|
|
||||||
val playlistWithSongs by remember(playlistId) {
|
|
||||||
Database.playlistWithSongs(playlistId).map { it ?: PlaylistWithSongs.NotFound }
|
|
||||||
}.collectAsState(initial = PlaylistWithSongs.Empty, context = Dispatchers.IO)
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(
|
|
||||||
lazyListState = lazyListState,
|
|
||||||
key = playlistWithSongs.songs,
|
|
||||||
onDragEnd = { fromIndex, toIndex ->
|
|
||||||
query {
|
|
||||||
Database.move(playlistWithSongs.playlist.id, fromIndex, toIndex)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extraItemCount = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
var isRenaming by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRenaming) {
|
|
||||||
TextFieldDialog(
|
|
||||||
hintText = "Enter the playlist name",
|
|
||||||
initialTextInput = playlistWithSongs.playlist.name,
|
|
||||||
onDismiss = { isRenaming = false },
|
|
||||||
onDone = { text ->
|
|
||||||
query {
|
|
||||||
Database.update(playlistWithSongs.playlist.copy(name = text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isDeleting by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDeleting) {
|
|
||||||
ConfirmationDialog(
|
|
||||||
text = "Do you really want to delete this playlist?",
|
|
||||||
onDismiss = { isDeleting = false },
|
|
||||||
onConfirm = {
|
|
||||||
query {
|
|
||||||
Database.delete(playlistWithSongs.playlist)
|
|
||||||
}
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ReorderingLazyColumn(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
Column {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 16.dp, bottom = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = playlistWithSongs.playlist.name,
|
|
||||||
style = typography.m.semiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = "${playlistWithSongs.songs.size} songs",
|
|
||||||
style = typography.xxs.semiBold.secondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = playlistWithSongs.songs.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
playlistWithSongs.songs
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ellipsis_horizontal),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
menuState.display {
|
|
||||||
Menu {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.enqueue,
|
|
||||||
text = "Enqueue",
|
|
||||||
isEnabled = playlistWithSongs.songs.isNotEmpty(),
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
binder?.player?.enqueue(
|
|
||||||
playlistWithSongs.songs.map(
|
|
||||||
DetailedSong::asMediaItem
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.pencil,
|
|
||||||
text = "Rename",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
isRenaming = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
playlistWithSongs.playlist.browseId?.let { browseId ->
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.sync,
|
|
||||||
text = "Sync",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
transaction {
|
|
||||||
runBlocking(Dispatchers.IO) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
YouTube.playlist(browseId)?.map {
|
|
||||||
it.next()
|
|
||||||
}?.map { playlist ->
|
|
||||||
playlist.copy(items = playlist.items?.filter { it.info.endpoint != null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.getOrNull()?.let { remotePlaylist ->
|
|
||||||
Database.clearPlaylist(playlistWithSongs.playlist.id)
|
|
||||||
|
|
||||||
remotePlaylist.items?.forEachIndexed { index, song ->
|
|
||||||
song.toMediaItem(browseId, remotePlaylist)?.let { mediaItem ->
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
|
|
||||||
Database.insert(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
playlistId = playlistId,
|
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.trash,
|
|
||||||
text = "Delete",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
isDeleting = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = playlistWithSongs.songs,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song },
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
song = song,
|
|
||||||
thumbnailSize = thumbnailSize,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
playlistWithSongs.songs.map(
|
|
||||||
DetailedSong::asMediaItem
|
|
||||||
), index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
InPlaylistMediaItemMenu(
|
|
||||||
playlistId = playlistId,
|
|
||||||
positionInPlaylist = index,
|
|
||||||
song = song
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.reorder),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { }
|
|
||||||
.reorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index
|
|
||||||
)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement(reorderingState = reorderingState)
|
|
||||||
.draggedItem(reorderingState = reorderingState, index = index)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,457 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
|
||||||
import it.vfsfitvnm.vimusic.transaction
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.bold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun PlaylistScreen(browseId: String) {
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
|
|
||||||
val thumbnailSizePx = Dimensions.thumbnails.playlist.px
|
|
||||||
val songThumbnailSizePx = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
var playlist by remember {
|
|
||||||
mutableStateOf<Result<YouTube.PlaylistOrAlbum>?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val onLoad = relaunchableEffect(Unit) {
|
|
||||||
playlist = withContext(Dispatchers.IO) {
|
|
||||||
YouTube.playlist(browseId)?.map {
|
|
||||||
it.next()
|
|
||||||
}?.map { playlist ->
|
|
||||||
playlist.copy(items = playlist.items?.filter { it.info.endpoint != null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
playlist?.getOrNull()?.let { playlist ->
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = playlist.thumbnail?.size(thumbnailSizePx),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.playlist)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
BasicText(
|
|
||||||
text = playlist.title ?: "Unknown",
|
|
||||||
style = typography.m.semiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = playlist.authors?.joinToString("") { it.name }
|
|
||||||
?: "",
|
|
||||||
style = typography.xs.secondary.semiBold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
playlist.year?.let { year ->
|
|
||||||
BasicText(
|
|
||||||
text = year,
|
|
||||||
style = typography.xs.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
binder?.stopRadio()
|
|
||||||
playlist.items
|
|
||||||
?.shuffled()
|
|
||||||
?.mapNotNull { song ->
|
|
||||||
song.toMediaItem(browseId, playlist)
|
|
||||||
}
|
|
||||||
?.let { mediaItems ->
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
mediaItems
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ellipsis_horizontal),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
menuState.display {
|
|
||||||
Menu {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.enqueue,
|
|
||||||
text = "Enqueue",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
playlist.items
|
|
||||||
?.mapNotNull { song ->
|
|
||||||
song.toMediaItem(
|
|
||||||
browseId,
|
|
||||||
playlist
|
|
||||||
)
|
|
||||||
}
|
|
||||||
?.let { mediaItems ->
|
|
||||||
binder?.player?.enqueue(
|
|
||||||
mediaItems
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.playlist,
|
|
||||||
text = "Import",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
transaction {
|
|
||||||
val playlistId =
|
|
||||||
Database.insert(
|
|
||||||
Playlist(
|
|
||||||
name = playlist.title
|
|
||||||
?: "Unknown",
|
|
||||||
browseId = browseId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
playlist.items?.forEachIndexed { index, song ->
|
|
||||||
song
|
|
||||||
.toMediaItem(
|
|
||||||
browseId,
|
|
||||||
playlist
|
|
||||||
)
|
|
||||||
?.let { mediaItem ->
|
|
||||||
Database.insert(
|
|
||||||
mediaItem
|
|
||||||
)
|
|
||||||
|
|
||||||
Database.insert(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
playlistId = playlistId,
|
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.share_social,
|
|
||||||
text = "Share",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
(playlist.url
|
|
||||||
?: "https://music.youtube.com/playlist?list=${
|
|
||||||
browseId.removePrefix(
|
|
||||||
"VL"
|
|
||||||
)
|
|
||||||
}").let { url ->
|
|
||||||
val sendIntent = Intent().apply {
|
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
type = "text/plain"
|
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
sendIntent,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: playlist?.exceptionOrNull()?.let { throwable ->
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = throwable.javaClass.canonicalName,
|
|
||||||
onRetry = onLoad
|
|
||||||
)
|
|
||||||
} ?: LoadingOrError()
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = playlist?.getOrNull()?.items ?: emptyList(),
|
|
||||||
contentType = { _, song -> song }
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
title = song.info.name,
|
|
||||||
authors = (song.authors
|
|
||||||
?: playlist?.getOrNull()?.authors)?.joinToString("") { it.name },
|
|
||||||
durationText = song.durationText,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
playlist?.getOrNull()?.items?.mapNotNull { song ->
|
|
||||||
song.toMediaItem(browseId, playlist?.getOrNull()!!)
|
|
||||||
}?.let { mediaItems ->
|
|
||||||
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
startContent = {
|
|
||||||
if (song.thumbnail == null) {
|
|
||||||
BasicText(
|
|
||||||
text = "${index + 1}",
|
|
||||||
style = typography.xs.secondary.bold.center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier
|
|
||||||
.width(36.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AsyncImage(
|
|
||||||
model = song.thumbnail!!.size(songThumbnailSizePx),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.song)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
NonQueuedMediaItemMenu(
|
|
||||||
mediaItem = song.toMediaItem(
|
|
||||||
browseId,
|
|
||||||
playlist?.getOrNull()!!
|
|
||||||
)
|
|
||||||
?: return@SongItem,
|
|
||||||
onDismiss = menuState::hide,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoadingOrError(
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = errorMessage,
|
|
||||||
onRetry = onRetry
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = colorPalette.shimmer, shape = ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.playlist)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
TextPlaceholder()
|
|
||||||
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repeat(3) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.6f - it * 0.1f)
|
|
||||||
.height(Dimensions.thumbnails.song)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 4.dp, horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(8.dp)
|
|
||||||
.background(color = Color.Black, shape = CircleShape)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
TextPlaceholder()
|
|
||||||
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +1,30 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import it.vfsfitvnm.route.Route0
|
import it.vfsfitvnm.route.Route0
|
||||||
import it.vfsfitvnm.route.Route1
|
import it.vfsfitvnm.route.Route1
|
||||||
import it.vfsfitvnm.route.RouteHandlerScope
|
import it.vfsfitvnm.route.RouteHandlerScope
|
||||||
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen
|
||||||
|
|
||||||
val aboutRoute = Route0("aboutRoute")
|
|
||||||
val albumRoute = Route1<String?>("albumRoute")
|
val albumRoute = Route1<String?>("albumRoute")
|
||||||
val appearanceSettingsRoute = Route0("appearanceSettingsRoute")
|
|
||||||
val artistRoute = Route1<String?>("artistRoute")
|
val artistRoute = Route1<String?>("artistRoute")
|
||||||
val backupAndRestoreRoute = Route0("backupAndRestoreRoute")
|
|
||||||
val builtInPlaylistRoute = Route1<BuiltInPlaylist>("builtInPlaylistRoute")
|
val builtInPlaylistRoute = Route1<BuiltInPlaylist>("builtInPlaylistRoute")
|
||||||
val cacheSettingsRoute = Route0("cacheSettingsRoute")
|
|
||||||
val intentUriRoute = Route1<Uri?>("intentUriRoute")
|
|
||||||
val localPlaylistRoute = Route1<Long?>("localPlaylistRoute")
|
val localPlaylistRoute = Route1<Long?>("localPlaylistRoute")
|
||||||
val otherSettingsRoute = Route0("otherSettingsRoute")
|
|
||||||
val playerSettingsRoute = Route0("playerSettingsRoute")
|
|
||||||
val playlistRoute = Route1<String?>("playlistRoute")
|
val playlistRoute = Route1<String?>("playlistRoute")
|
||||||
val searchResultRoute = Route1<String>("searchResultRoute")
|
val searchResultRoute = Route1<String>("searchResultRoute")
|
||||||
val searchRoute = Route1<String>("searchRoute")
|
val searchRoute = Route1<String>("searchRoute")
|
||||||
val settingsRoute = Route0("settingsRoute")
|
val settingsRoute = Route0("settingsRoute")
|
||||||
val viewPlaylistsRoute = Route0("createPlaylistRoute")
|
|
||||||
|
|
||||||
@SuppressLint("ComposableNaming")
|
@SuppressLint("ComposableNaming")
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
|
@ExperimentalFoundationApi
|
||||||
@Composable
|
@Composable
|
||||||
inline fun RouteHandlerScope.globalRoutes() {
|
inline fun RouteHandlerScope.globalRoutes() {
|
||||||
albumRoute { browseId ->
|
albumRoute { browseId ->
|
||||||
|
@ -42,4 +38,10 @@ inline fun RouteHandlerScope.globalRoutes() {
|
||||||
browseId = browseId ?: error("browseId cannot be null")
|
browseId = browseId ?: error("browseId cannot be null")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playlistRoute { browseId ->
|
||||||
|
PlaylistScreen(
|
||||||
|
browseId = browseId ?: error("browseId cannot be null")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,601 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.ChipGroup
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.ChipItem
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
|
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
|
||||||
import it.vfsfitvnm.vimusic.utils.searchFilterKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
var searchFilter by rememberPreference(searchFilterKey, YouTube.Item.Song.Filter.value)
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
val items = remember(searchFilter) {
|
|
||||||
mutableStateListOf<YouTube.Item>()
|
|
||||||
}
|
|
||||||
|
|
||||||
var continuationResult by remember(searchFilter) {
|
|
||||||
mutableStateOf<Result<String?>?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val onLoad = relaunchableEffect(searchFilter) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
val token = continuationResult?.getOrNull()
|
|
||||||
|
|
||||||
continuationResult = null
|
|
||||||
|
|
||||||
continuationResult = withContext(Dispatchers.IO) {
|
|
||||||
YouTube.search(query, searchFilter, token)
|
|
||||||
}?.map { searchResult ->
|
|
||||||
items.addAll(searchResult.items)
|
|
||||||
searchResult.continuation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val thumbnailSizePx = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
albumRoute { browseId ->
|
|
||||||
AlbumScreen(
|
|
||||||
browseId = browseId ?: "browseId cannot be null"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
artistRoute { browseId ->
|
|
||||||
ArtistScreen(
|
|
||||||
browseId = browseId ?: "browseId cannot be null"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
playlistRoute { browseId ->
|
|
||||||
PlaylistScreen(
|
|
||||||
browseId = browseId ?: "browseId cannot be null"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
host {
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = query,
|
|
||||||
style = typography.m.semiBold.center,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = onSearchAgain
|
|
||||||
)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
ChipGroup(
|
|
||||||
items = listOf(
|
|
||||||
ChipItem(
|
|
||||||
text = "Songs",
|
|
||||||
value = YouTube.Item.Song.Filter.value
|
|
||||||
),
|
|
||||||
ChipItem(
|
|
||||||
text = "Albums",
|
|
||||||
value = YouTube.Item.Album.Filter.value
|
|
||||||
),
|
|
||||||
ChipItem(
|
|
||||||
text = "Artists",
|
|
||||||
value = YouTube.Item.Artist.Filter.value
|
|
||||||
),
|
|
||||||
ChipItem(
|
|
||||||
text = "Videos",
|
|
||||||
value = YouTube.Item.Video.Filter.value
|
|
||||||
),
|
|
||||||
ChipItem(
|
|
||||||
text = "Playlists",
|
|
||||||
value = YouTube.Item.CommunityPlaylist.Filter.value
|
|
||||||
),
|
|
||||||
ChipItem(
|
|
||||||
text = "Featured playlists",
|
|
||||||
value = YouTube.Item.FeaturedPlaylist.Filter.value
|
|
||||||
),
|
|
||||||
),
|
|
||||||
value = searchFilter,
|
|
||||||
selectedBackgroundColor = colorPalette.accent,
|
|
||||||
unselectedBackgroundColor = colorPalette.background1,
|
|
||||||
selectedTextStyle = typography.xs.medium.color(colorPalette.onAccent),
|
|
||||||
unselectedTextStyle = typography.xs.medium,
|
|
||||||
shape = RoundedCornerShape(36.dp),
|
|
||||||
onValueChanged = {
|
|
||||||
searchFilter = it
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = items,
|
|
||||||
contentType = { it }
|
|
||||||
) { item ->
|
|
||||||
SmallItem(
|
|
||||||
item = item,
|
|
||||||
thumbnailSizeDp = Dimensions.thumbnails.song,
|
|
||||||
thumbnailSizePx = thumbnailSizePx,
|
|
||||||
onClick = {
|
|
||||||
when (item) {
|
|
||||||
is YouTube.Item.Album -> albumRoute(item.info.endpoint!!.browseId)
|
|
||||||
is YouTube.Item.Artist -> artistRoute(item.info.endpoint!!.browseId)
|
|
||||||
is YouTube.Item.Playlist -> playlistRoute(item.info.endpoint!!.browseId)
|
|
||||||
is YouTube.Item.Song -> {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlay(item.asMediaItem)
|
|
||||||
binder?.setupRadio(item.info.endpoint)
|
|
||||||
}
|
|
||||||
is YouTube.Item.Video -> {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlay(item.asMediaItem)
|
|
||||||
binder?.setupRadio(item.info.endpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
continuationResult?.getOrNull()?.let {
|
|
||||||
if (items.isNotEmpty()) {
|
|
||||||
item {
|
|
||||||
SideEffect(onLoad)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: continuationResult?.exceptionOrNull()?.let { throwable ->
|
|
||||||
item {
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = throwable.javaClass.canonicalName,
|
|
||||||
onRetry = onLoad
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: continuationResult?.let {
|
|
||||||
if (items.isEmpty()) {
|
|
||||||
item {
|
|
||||||
TextCard(icon = R.drawable.sad) {
|
|
||||||
Title(text = "No results found")
|
|
||||||
Text(text = "Please try a different query or category.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: item(key = "loading") {
|
|
||||||
LoadingOrError(
|
|
||||||
itemCount = if (items.isEmpty()) 8 else 3,
|
|
||||||
isLoadingArtists = searchFilter == YouTube.Item.Artist.Filter.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SmallSongItemShimmer(
|
|
||||||
thumbnailSizeDp: Dp,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
|
||||||
.size(thumbnailSizeDp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column {
|
|
||||||
TextPlaceholder()
|
|
||||||
TextPlaceholder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SmallArtistItemShimmer(
|
|
||||||
thumbnailSizeDp: Dp,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = colorPalette.shimmer, shape = CircleShape)
|
|
||||||
.size(thumbnailSizeDp)
|
|
||||||
)
|
|
||||||
|
|
||||||
TextPlaceholder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun SmallItem(
|
|
||||||
item: YouTube.Item,
|
|
||||||
thumbnailSizeDp: Dp,
|
|
||||||
thumbnailSizePx: Int,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
when (item) {
|
|
||||||
is YouTube.Item.Artist -> SmallArtistItem(
|
|
||||||
artist = item,
|
|
||||||
thumbnailSizeDp = thumbnailSizeDp,
|
|
||||||
thumbnailSizePx = thumbnailSizePx,
|
|
||||||
modifier = modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = onClick
|
|
||||||
)
|
|
||||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
is YouTube.Item.Song -> SmallSongItem(
|
|
||||||
song = item,
|
|
||||||
thumbnailSizePx = thumbnailSizePx,
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
is YouTube.Item.Album -> SmallAlbumItem(
|
|
||||||
album = item,
|
|
||||||
thumbnailSizeDp = thumbnailSizeDp,
|
|
||||||
thumbnailSizePx = thumbnailSizePx,
|
|
||||||
modifier = modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = onClick
|
|
||||||
)
|
|
||||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
is YouTube.Item.Video -> SmallVideoItem(
|
|
||||||
video = item,
|
|
||||||
thumbnailSizePx = thumbnailSizePx,
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
is YouTube.Item.Playlist -> SmallPlaylistItem(
|
|
||||||
playlist = item,
|
|
||||||
thumbnailSizeDp = thumbnailSizeDp,
|
|
||||||
thumbnailSizePx = thumbnailSizePx,
|
|
||||||
modifier = modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = onClick
|
|
||||||
)
|
|
||||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun SmallSongItem(
|
|
||||||
song: YouTube.Item.Song,
|
|
||||||
thumbnailSizePx: Int,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
SongItem(
|
|
||||||
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
|
|
||||||
title = song.info.name,
|
|
||||||
authors = song.authors.joinToString("") { it.name },
|
|
||||||
durationText = song.durationText,
|
|
||||||
onClick = onClick,
|
|
||||||
menuContent = {
|
|
||||||
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
|
||||||
},
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun SmallVideoItem(
|
|
||||||
video: YouTube.Item.Video,
|
|
||||||
thumbnailSizePx: Int,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
SongItem(
|
|
||||||
thumbnailModel = video.thumbnail?.size(thumbnailSizePx),
|
|
||||||
title = video.info.name,
|
|
||||||
authors = (if (video.isOfficialMusicVideo) video.authors else video.views)
|
|
||||||
.joinToString("") { it.name },
|
|
||||||
durationText = video.durationText,
|
|
||||||
onClick = onClick,
|
|
||||||
menuContent = {
|
|
||||||
NonQueuedMediaItemMenu(mediaItem = video.asMediaItem)
|
|
||||||
},
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun SmallPlaylistItem(
|
|
||||||
playlist: YouTube.Item.Playlist,
|
|
||||||
thumbnailSizeDp: Dp,
|
|
||||||
thumbnailSizePx: Int,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = playlist.thumbnail?.size(thumbnailSizePx),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(ThumbnailRoundness.shape)
|
|
||||||
.size(thumbnailSizeDp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = playlist.info.name,
|
|
||||||
style = typography.xs.semiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = playlist.channel?.name ?: "",
|
|
||||||
style = typography.xs.semiBold.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
playlist.songCount?.let { songCount ->
|
|
||||||
BasicText(
|
|
||||||
text = "$songCount songs",
|
|
||||||
style = typography.xxs.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SmallAlbumItem(
|
|
||||||
album: YouTube.Item.Album,
|
|
||||||
thumbnailSizeDp: Dp,
|
|
||||||
thumbnailSizePx: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = album.thumbnail?.size(thumbnailSizePx),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(ThumbnailRoundness.shape)
|
|
||||||
.size(thumbnailSizeDp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = album.info.name,
|
|
||||||
style = typography.xs.semiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = album.authors?.joinToString("") { it.name } ?: "",
|
|
||||||
style = typography.xs.semiBold.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
album.year?.let { year ->
|
|
||||||
BasicText(
|
|
||||||
text = year,
|
|
||||||
style = typography.xxs.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SmallArtistItem(
|
|
||||||
artist: YouTube.Item.Artist,
|
|
||||||
thumbnailSizeDp: Dp,
|
|
||||||
thumbnailSizePx: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
modifier = modifier
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = artist.thumbnail?.size(thumbnailSizePx),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(CircleShape)
|
|
||||||
.size(thumbnailSizeDp)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = artist.info.name,
|
|
||||||
style = typography.xs.semiBold,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoadingOrError(
|
|
||||||
itemCount: Int = 0,
|
|
||||||
isLoadingArtists: Boolean = false,
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = errorMessage,
|
|
||||||
onRetry = onRetry,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
repeat(itemCount) { index ->
|
|
||||||
if (isLoadingArtists) {
|
|
||||||
SmallArtistItemShimmer(
|
|
||||||
thumbnailSizeDp = Dimensions.thumbnails.song,
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(1f - index * 0.125f)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
SmallSongItemShimmer(
|
|
||||||
thumbnailSizeDp = Dimensions.thumbnails.song,
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(1f - index * 0.125f)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,400 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.systemBars
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.paint
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.models.SearchQuery
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (Uri) -> Unit) {
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
|
||||||
val paddingValues = WindowInsets.systemBars.asPaddingValues()
|
|
||||||
|
|
||||||
val timeIconPainter = painterResource(R.drawable.time)
|
|
||||||
val closeIconPainter = painterResource(R.drawable.close)
|
|
||||||
val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward)
|
|
||||||
val rippleIndication = rememberRipple(bounded = true)
|
|
||||||
|
|
||||||
var textFieldValue by rememberSaveable(
|
|
||||||
initialTextInput,
|
|
||||||
stateSaver = TextFieldValue.Saver
|
|
||||||
) {
|
|
||||||
mutableStateOf(
|
|
||||||
TextFieldValue(
|
|
||||||
text = initialTextInput,
|
|
||||||
selection = TextRange(initialTextInput.length)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val focusRequester = remember {
|
|
||||||
FocusRequester()
|
|
||||||
}
|
|
||||||
|
|
||||||
val searchSuggestionsResult by produceState<Result<List<String>?>?>(
|
|
||||||
initialValue = null,
|
|
||||||
key1 = textFieldValue
|
|
||||||
) {
|
|
||||||
value = if (textFieldValue.text.isNotEmpty()) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
YouTube.getSearchSuggestions(textFieldValue.text)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val history by remember(textFieldValue.text) {
|
|
||||||
Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new ->
|
|
||||||
old.size == new.size
|
|
||||||
}
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
val isOpenableUrl = remember(textFieldValue.text) {
|
|
||||||
listOf(
|
|
||||||
"https://www.youtube.com/watch?",
|
|
||||||
"https://music.youtube.com/watch?",
|
|
||||||
"https://m.youtube.com/watch?",
|
|
||||||
"https://www.youtube.com/playlist?",
|
|
||||||
"https://music.youtube.com/playlist?",
|
|
||||||
"https://m.youtube.com/playlist?",
|
|
||||||
"https://youtu.be/",
|
|
||||||
).any(textFieldValue.text::startsWith)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
delay(300)
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.imePadding()
|
|
||||||
.padding(
|
|
||||||
start = paddingValues.calculateStartPadding(layoutDirection),
|
|
||||||
end = paddingValues.calculateEndPadding(layoutDirection),
|
|
||||||
top = paddingValues.calculateTopPadding(),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = textFieldValue,
|
|
||||||
onValueChange = {
|
|
||||||
textFieldValue = it
|
|
||||||
},
|
|
||||||
textStyle = typography.m.medium,
|
|
||||||
singleLine = true,
|
|
||||||
maxLines = 1,
|
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onSearch = {
|
|
||||||
if (textFieldValue.text.isNotEmpty()) {
|
|
||||||
onSearch(textFieldValue.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
cursorBrush = SolidColor(colorPalette.text),
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
pop()
|
|
||||||
focusRequester.freeFocus()
|
|
||||||
}
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
|
||||||
visible = textFieldValue.text.isEmpty(),
|
|
||||||
enter = fadeIn(tween(100)),
|
|
||||||
exit = fadeOut(tween(100)),
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Enter a song, an album, an artist name...",
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = typography.m.secondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
textFieldValue = TextFieldValue()
|
|
||||||
}
|
|
||||||
.padding(horizontal = 14.dp, vertical = 6.dp)
|
|
||||||
.background(
|
|
||||||
color = colorPalette.background1,
|
|
||||||
shape = CircleShape
|
|
||||||
)
|
|
||||||
.size(28.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.close),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(14.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOpenableUrl) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rippleIndication,
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { onUri(textFieldValue.text.toUri()) }
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(colorPalette.background1)
|
|
||||||
.padding(vertical = 16.dp, horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.link),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = "Open URL",
|
|
||||||
style = typography.s.secondary,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = PaddingValues(
|
|
||||||
bottom = Dimensions.collapsedPlayer + paddingValues.calculateBottomPadding()
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
items = history,
|
|
||||||
key = SearchQuery::id
|
|
||||||
) { searchQuery ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rippleIndication,
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { onSearch(searchQuery.query) }
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp, horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
.paint(
|
|
||||||
painter = timeIconPainter,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = searchQuery.query,
|
|
||||||
style = typography.s.secondary,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
query {
|
|
||||||
Database.delete(searchQuery)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
.paint(
|
|
||||||
painter = closeIconPainter,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
textFieldValue = TextFieldValue(
|
|
||||||
text = searchQuery.query,
|
|
||||||
selection = TextRange(searchQuery.query.length)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.rotate(225f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
.paint(
|
|
||||||
painter = arrowForwardIconPainter,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchSuggestionsResult?.getOrNull()?.let { suggestions ->
|
|
||||||
items(items = suggestions) { suggestion ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rippleIndication,
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { onSearch(suggestion) }
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp, horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = suggestion,
|
|
||||||
style = typography.s.secondary,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
textFieldValue = TextFieldValue(
|
|
||||||
text = suggestion,
|
|
||||||
selection = TextRange(suggestion.length)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.rotate(225f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.size(22.dp)
|
|
||||||
.paint(
|
|
||||||
painter = arrowForwardIconPainter,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textDisabled)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: searchSuggestionsResult?.exceptionOrNull()?.let { throwable ->
|
|
||||||
item {
|
|
||||||
LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,425 +0,0 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.foundation.*
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.*
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import it.vfsfitvnm.route.*
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.badge
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Switch
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.ValueSelectorDialog
|
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.settings.*
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.utils.*
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun SettingsScreen() {
|
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
|
|
||||||
RouteHandler(
|
|
||||||
listenToGlobalEmitter = true,
|
|
||||||
transitionSpec = {
|
|
||||||
when (targetState.route) {
|
|
||||||
albumRoute, artistRoute -> fastFade
|
|
||||||
else -> when (initialState.route) {
|
|
||||||
albumRoute, artistRoute -> fastFade
|
|
||||||
null -> leftSlide
|
|
||||||
else -> rightSlide
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
appearanceSettingsRoute {
|
|
||||||
AppearanceSettingsScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
playerSettingsRoute {
|
|
||||||
PlayerSettingsScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
backupAndRestoreRoute {
|
|
||||||
BackupAndRestoreScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheSettingsRoute {
|
|
||||||
CacheSettingsScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
otherSettingsRoute {
|
|
||||||
OtherSettingsScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
aboutRoute {
|
|
||||||
AboutScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
host {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
var isFirstLaunch by rememberPreference(isFirstLaunchKey, true)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.padding(LocalPlayerAwarePaddingValues.current)
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.chevron_back),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = pop)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = "Settings",
|
|
||||||
style = typography.l.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 48.dp)
|
|
||||||
.padding(all = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Entry(
|
|
||||||
@DrawableRes icon: Int,
|
|
||||||
color: Color,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
route: Route0,
|
|
||||||
withAlert: Boolean = false,
|
|
||||||
onClick: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = {
|
|
||||||
route()
|
|
||||||
onClick?.invoke()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(color = color, shape = CircleShape)
|
|
||||||
.size(36.dp)
|
|
||||||
.badge(color = colorPalette.red, isDisplayed = withAlert)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(icon),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = title,
|
|
||||||
style = typography.s.semiBold,
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = description,
|
|
||||||
style = typography.xs.secondary.medium,
|
|
||||||
maxLines = 2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Entry(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
icon = R.drawable.color_palette,
|
|
||||||
title = "Appearance",
|
|
||||||
description = "Change the colors and shapes",
|
|
||||||
route = appearanceSettingsRoute,
|
|
||||||
)
|
|
||||||
|
|
||||||
Entry(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
icon = R.drawable.play,
|
|
||||||
title = "Player & Audio",
|
|
||||||
description = "Player and audio settings",
|
|
||||||
route = playerSettingsRoute,
|
|
||||||
)
|
|
||||||
|
|
||||||
Entry(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
icon = R.drawable.server,
|
|
||||||
title = "Cache",
|
|
||||||
description = "Manage the used space",
|
|
||||||
route = cacheSettingsRoute
|
|
||||||
)
|
|
||||||
|
|
||||||
Entry(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
icon = R.drawable.save,
|
|
||||||
title = "Backup & Restore",
|
|
||||||
description = "Backup and restore the database",
|
|
||||||
route = backupAndRestoreRoute
|
|
||||||
)
|
|
||||||
|
|
||||||
Entry(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
icon = R.drawable.shapes,
|
|
||||||
title = "Other",
|
|
||||||
description = "Advanced settings",
|
|
||||||
route = otherSettingsRoute,
|
|
||||||
withAlert = isFirstLaunch,
|
|
||||||
onClick = {
|
|
||||||
isFirstLaunch = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Entry(
|
|
||||||
color = colorPalette.background2,
|
|
||||||
icon = R.drawable.information,
|
|
||||||
title = "About",
|
|
||||||
description = "App version and social links",
|
|
||||||
route = aboutRoute
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
inline fun <reified T : Enum<T>> EnumValueSelectorSettingsEntry(
|
|
||||||
title: String,
|
|
||||||
selectedValue: T,
|
|
||||||
crossinline onValueSelected: (T) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isEnabled: Boolean = true,
|
|
||||||
crossinline valueText: (T) -> String = Enum<T>::name
|
|
||||||
) {
|
|
||||||
ValueSelectorSettingsEntry(
|
|
||||||
title = title,
|
|
||||||
selectedValue = selectedValue,
|
|
||||||
values = enumValues<T>().toList(),
|
|
||||||
onValueSelected = onValueSelected,
|
|
||||||
modifier = modifier,
|
|
||||||
isEnabled = isEnabled,
|
|
||||||
valueText = valueText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
inline fun <T> ValueSelectorSettingsEntry(
|
|
||||||
title: String,
|
|
||||||
selectedValue: T,
|
|
||||||
values: List<T>,
|
|
||||||
crossinline onValueSelected: (T) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isEnabled: Boolean = true,
|
|
||||||
crossinline valueText: (T) -> String = { it.toString() }
|
|
||||||
) {
|
|
||||||
var isShowingDialog by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isShowingDialog) {
|
|
||||||
ValueSelectorDialog(
|
|
||||||
onDismiss = {
|
|
||||||
isShowingDialog = false
|
|
||||||
},
|
|
||||||
title = title,
|
|
||||||
selectedValue = selectedValue,
|
|
||||||
values = values,
|
|
||||||
onValueSelected = onValueSelected,
|
|
||||||
valueText = valueText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsEntry(
|
|
||||||
title = title,
|
|
||||||
text = valueText(selectedValue),
|
|
||||||
modifier = modifier,
|
|
||||||
isEnabled = isEnabled,
|
|
||||||
onClick = {
|
|
||||||
isShowingDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SwitchSettingEntry(
|
|
||||||
title: String,
|
|
||||||
text: String,
|
|
||||||
isChecked: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isEnabled: Boolean = true
|
|
||||||
) {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { onCheckedChange(!isChecked) },
|
|
||||||
enabled = isEnabled
|
|
||||||
)
|
|
||||||
.alpha(if (isEnabled) 1f else 0.5f)
|
|
||||||
.padding(start = 24.dp)
|
|
||||||
.padding(horizontal = 32.dp, vertical = 16.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = title,
|
|
||||||
style = typography.xs.semiBold.copy(color = colorPalette.text),
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xs.semiBold.copy(color = colorPalette.textSecondary),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Switch(isChecked = isChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsEntry(
|
|
||||||
title: String,
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
isEnabled: Boolean = true
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = onClick,
|
|
||||||
enabled = isEnabled
|
|
||||||
)
|
|
||||||
.alpha(if (isEnabled) 1f else 0.5f)
|
|
||||||
.padding(start = 24.dp)
|
|
||||||
.padding(horizontal = 32.dp, vertical = 16.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = title,
|
|
||||||
style = typography.xs.semiBold.copy(color = colorPalette.text),
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xs.semiBold.copy(color = colorPalette.textSecondary),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsTitle(
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(start = 40.dp)
|
|
||||||
.padding(all = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsDescription(
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xxs.secondary,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(start = 56.dp, end = 24.dp)
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsGroupDescription(
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xxs.secondary,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(start = 56.dp, end = 24.dp)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SettingsEntryGroupText(
|
|
||||||
title: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = title.uppercase(),
|
|
||||||
style = typography.xxs.semiBold.copy(colorPalette.accent),
|
|
||||||
modifier = modifier
|
|
||||||
.padding(start = 24.dp, top = 24.dp)
|
|
||||||
.padding(horizontal = 32.dp)
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
|
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
||||||
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.AlbumSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.InnertubeAlbumItemListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.innertubeItemsPageSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.albumPage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun AlbumScreen(browseId: String) {
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
|
val (tabIndex, onTabChanged) = rememberSaveable {
|
||||||
|
mutableStateOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val album by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(AlbumSaver),
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.album(browseId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val innertubeAlbum by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver),
|
||||||
|
tabIndex > 0
|
||||||
|
) {
|
||||||
|
if (value != null || (tabIndex == 0 && withContext(Dispatchers.IO) {
|
||||||
|
Database.albumTimestamp(
|
||||||
|
browseId
|
||||||
|
)
|
||||||
|
} != null)) return@produceSaveableState
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Innertube.albumPage(BrowseBody(browseId = browseId))
|
||||||
|
}?.onSuccess { albumPage ->
|
||||||
|
value = albumPage
|
||||||
|
|
||||||
|
query {
|
||||||
|
Database.upsert(
|
||||||
|
Album(
|
||||||
|
id = browseId,
|
||||||
|
title = albumPage.title,
|
||||||
|
thumbnailUrl = albumPage.thumbnail?.url,
|
||||||
|
year = albumPage.year,
|
||||||
|
authorsText = albumPage.authors?.joinToString("") { it.name ?: "" },
|
||||||
|
shareUrl = albumPage.url,
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
bookmarkedAt = album?.bookmarkedAt
|
||||||
|
),
|
||||||
|
albumPage
|
||||||
|
.songsPage
|
||||||
|
?.items
|
||||||
|
?.map(Innertube.SongItem::asMediaItem)
|
||||||
|
?.onEach(Database::insert)
|
||||||
|
?.mapIndexed { position, mediaItem ->
|
||||||
|
SongAlbumMap(
|
||||||
|
songId = mediaItem.mediaId,
|
||||||
|
albumId = browseId,
|
||||||
|
position = position
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
|
globalRoutes()
|
||||||
|
|
||||||
|
host {
|
||||||
|
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit =
|
||||||
|
{ textButton ->
|
||||||
|
if (album?.timestamp == null) {
|
||||||
|
HeaderPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.shimmer()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Header(title = album?.title ?: "Unknown") {
|
||||||
|
textButton?.invoke()
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = if (album?.bookmarkedAt == null) {
|
||||||
|
R.drawable.bookmark_outline
|
||||||
|
} else {
|
||||||
|
R.drawable.bookmark
|
||||||
|
},
|
||||||
|
color = colorPalette.accent,
|
||||||
|
onClick = {
|
||||||
|
val bookmarkedAt =
|
||||||
|
if (album?.bookmarkedAt == null) System.currentTimeMillis() else null
|
||||||
|
|
||||||
|
query {
|
||||||
|
album
|
||||||
|
?.copy(bookmarkedAt = bookmarkedAt)
|
||||||
|
?.let(Database::update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.share_social,
|
||||||
|
color = colorPalette.text,
|
||||||
|
onClick = {
|
||||||
|
album?.shareUrl?.let { url ->
|
||||||
|
val sendIntent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_TEXT, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
sendIntent,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnailContent =
|
||||||
|
adaptiveThumbnailContent(album?.timestamp == null, album?.thumbnailUrl)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
|
onTopIconButtonClick = pop,
|
||||||
|
tabIndex = tabIndex,
|
||||||
|
onTabChanged = onTabChanged,
|
||||||
|
tabColumnContent = { Item ->
|
||||||
|
Item(0, "Songs", R.drawable.musical_notes)
|
||||||
|
Item(1, "Other versions", R.drawable.disc)
|
||||||
|
}
|
||||||
|
) { currentTabIndex ->
|
||||||
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
|
when (currentTabIndex) {
|
||||||
|
0 -> AlbumSongs(
|
||||||
|
browseId = browseId,
|
||||||
|
headerContent = headerContent,
|
||||||
|
thumbnailContent = thumbnailContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
val thumbnailSizeDp = 108.dp
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
ItemsPage(
|
||||||
|
stateSaver = innertubeItemsPageSaver(InnertubeAlbumItemListSaver),
|
||||||
|
headerContent = headerContent,
|
||||||
|
initialPlaceholderCount = 1,
|
||||||
|
continuationPlaceholderCount = 1,
|
||||||
|
emptyItemsText = "This album doesn't have any alternative version",
|
||||||
|
itemsPageProvider = innertubeAlbum?.let {
|
||||||
|
({
|
||||||
|
Result.success(
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
items = innertubeAlbum?.otherVersions,
|
||||||
|
continuation = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
itemContent = { album ->
|
||||||
|
AlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { albumRoute(album.key) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
itemPlaceholderContent = {
|
||||||
|
AlbumItemPlaceholder(thumbnailSizeDp = thumbnailSizeDp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
|
import it.vfsfitvnm.vimusic.utils.isLandscape
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun AlbumSongs(
|
||||||
|
browseId: String,
|
||||||
|
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||||
|
thumbnailContent: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
|
||||||
|
val songs by produceSaveableState(
|
||||||
|
initialValue = emptyList(),
|
||||||
|
stateSaver = DetailedSongListSaver
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.albumSongs(browseId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
|
||||||
|
Box {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
headerContent {
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "Enqueue",
|
||||||
|
enabled = songs.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLandscape) {
|
||||||
|
thumbnailContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = songs,
|
||||||
|
key = { _, song -> song.id }
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
title = song.title,
|
||||||
|
authors = song.artistsText,
|
||||||
|
duration = song.durationText,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
thumbnailContent = {
|
||||||
|
BasicText(
|
||||||
|
text = "${index + 1}",
|
||||||
|
style = typography.s.semiBold.center.color(colorPalette.textDisabled),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(thumbnailSizeDp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
NonQueuedMediaItemMenu(
|
||||||
|
onDismiss = menuState::hide,
|
||||||
|
mediaItem = song.asMediaItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(
|
||||||
|
songs.map(DetailedSong::asMediaItem),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songs.isEmpty()) {
|
||||||
|
item(key = "loading") {
|
||||||
|
ShimmerHost(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillParentMaxSize()
|
||||||
|
) {
|
||||||
|
repeat(4) {
|
||||||
|
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
iconId = R.drawable.shuffle,
|
||||||
|
onClick = {
|
||||||
|
if (songs.isNotEmpty()) {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(
|
||||||
|
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.artist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun ArtistLocalSongs(
|
||||||
|
browseId: String,
|
||||||
|
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||||
|
thumbnailContent: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
|
||||||
|
val songs by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(DetailedSongListSaver)
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.artistSongs(browseId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
|
||||||
|
Box {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
headerContent {
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "Enqueue",
|
||||||
|
enabled = !songs.isNullOrEmpty(),
|
||||||
|
onClick = {
|
||||||
|
binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnailContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
songs?.let { songs ->
|
||||||
|
itemsIndexed(
|
||||||
|
items = songs,
|
||||||
|
key = { _, song -> song.id }
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
thumbnailSizePx = songThumbnailSizePx,
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
NonQueuedMediaItemMenu(
|
||||||
|
onDismiss = menuState::hide,
|
||||||
|
mediaItem = song.asMediaItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(
|
||||||
|
songs.map(DetailedSong::asMediaItem),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: item(key = "loading") {
|
||||||
|
ShimmerHost {
|
||||||
|
repeat(4) {
|
||||||
|
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
iconId = R.drawable.shuffle,
|
||||||
|
onClick = {
|
||||||
|
songs?.let { songs ->
|
||||||
|
if (songs.isNotEmpty()) {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(
|
||||||
|
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.artist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun ArtistOverview(
|
||||||
|
youtubeArtistPage: Innertube.ArtistPage?,
|
||||||
|
onViewAllSongsClick: () -> Unit,
|
||||||
|
onViewAllAlbumsClick: () -> Unit,
|
||||||
|
onViewAllSinglesClick: () -> Unit,
|
||||||
|
onAlbumClick: (String) -> Unit,
|
||||||
|
thumbnailContent: @Composable () -> Unit,
|
||||||
|
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
val windowInsets = LocalPlayerAwareWindowInsets.current
|
||||||
|
|
||||||
|
val songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||||
|
val albumThumbnailSizeDp = 108.dp
|
||||||
|
val albumThumbnailSizePx = albumThumbnailSizeDp.px
|
||||||
|
|
||||||
|
val endPaddingValues = windowInsets.only(WindowInsetsSides.End).asPaddingValues()
|
||||||
|
|
||||||
|
val sectionTextModifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(top = 24.dp, bottom = 8.dp)
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
|
||||||
|
Box {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(windowInsets.only(WindowInsetsSides.Vertical).asPaddingValues())
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(endPaddingValues)
|
||||||
|
) {
|
||||||
|
headerContent {
|
||||||
|
youtubeArtistPage?.shuffleEndpoint?.let { endpoint ->
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "Shuffle",
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.playRadio(endpoint)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnailContent()
|
||||||
|
|
||||||
|
if (youtubeArtistPage != null) {
|
||||||
|
youtubeArtistPage.songs?.let { songs ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(endPaddingValues)
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Songs",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
youtubeArtistPage.songsEndpoint?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "View all",
|
||||||
|
style = typography.xs.secondary,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
.clickable(onClick = onViewAllSongsClick),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
songs.forEach { song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
thumbnailSizePx = songThumbnailSizePx,
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
NonQueuedMediaItemMenu(
|
||||||
|
onDismiss = menuState::hide,
|
||||||
|
mediaItem = song.asMediaItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val mediaItem = song.asMediaItem
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlay(mediaItem)
|
||||||
|
binder?.setupRadio(
|
||||||
|
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(endPaddingValues)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeArtistPage.albums?.let { albums ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(endPaddingValues)
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Albums",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
youtubeArtistPage.albumsEndpoint?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "View all",
|
||||||
|
style = typography.xs.secondary,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
.clickable(onClick = onViewAllAlbumsClick),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = endPaddingValues,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = albums,
|
||||||
|
key = Innertube.AlbumItem::key
|
||||||
|
) { album ->
|
||||||
|
AlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = albumThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onAlbumClick(album.key) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeArtistPage.singles?.let { singles ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(endPaddingValues)
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Singles",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
youtubeArtistPage.singlesEndpoint?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "View all",
|
||||||
|
style = typography.xs.secondary,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
.clickable(onClick = onViewAllSinglesClick),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = endPaddingValues,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = singles,
|
||||||
|
key = Innertube.AlbumItem::key
|
||||||
|
) { album ->
|
||||||
|
AlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = albumThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onAlbumClick(album.key) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ShimmerHost {
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
repeat(5) {
|
||||||
|
SongItemPlaceholder(
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat(2) {
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
repeat(2) {
|
||||||
|
AlbumItemPlaceholder(
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
alternative = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
youtubeArtistPage?.radioEndpoint?.let { endpoint ->
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
scrollState = scrollState,
|
||||||
|
iconId = R.drawable.radio,
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.playRadio(endpoint)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,381 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.artist
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.ArtistSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.InnertubeAlbumsPageSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.InnertubeArtistPageSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.InnertubeSongsPageSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.artistScreenTabIndexKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.artistPage
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.itemsPage
|
||||||
|
import it.vfsfitvnm.youtubemusic.utils.from
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun ArtistScreen(browseId: String) {
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
|
val (tabIndex, onTabIndexChanged) = rememberPreference(
|
||||||
|
artistScreenTabIndexKey,
|
||||||
|
defaultValue = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
val artist by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(ArtistSaver),
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.artist(browseId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val youtubeArtist by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(InnertubeArtistPageSaver),
|
||||||
|
tabIndex < 4
|
||||||
|
) {
|
||||||
|
if (value != null || (tabIndex == 4 && withContext(Dispatchers.IO) {
|
||||||
|
Database.artistTimestamp(
|
||||||
|
browseId
|
||||||
|
)
|
||||||
|
} != null)) return@produceSaveableState
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Innertube.artistPage(BrowseBody(browseId = browseId))
|
||||||
|
}?.onSuccess { artistPage ->
|
||||||
|
value = artistPage
|
||||||
|
|
||||||
|
query {
|
||||||
|
Database.upsert(
|
||||||
|
Artist(
|
||||||
|
id = browseId,
|
||||||
|
name = artistPage.name,
|
||||||
|
thumbnailUrl = artistPage.thumbnail?.url,
|
||||||
|
info = artistPage.description,
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
bookmarkedAt = artist?.bookmarkedAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
|
globalRoutes()
|
||||||
|
|
||||||
|
host {
|
||||||
|
val thumbnailContent =
|
||||||
|
adaptiveThumbnailContent(
|
||||||
|
artist?.timestamp == null,
|
||||||
|
artist?.thumbnailUrl,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
|
||||||
|
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit =
|
||||||
|
{ textButton ->
|
||||||
|
if (artist?.timestamp == null) {
|
||||||
|
HeaderPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.shimmer()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Header(title = artist?.name ?: "Unknown") {
|
||||||
|
textButton?.invoke()
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = if (artist?.bookmarkedAt == null) {
|
||||||
|
R.drawable.bookmark_outline
|
||||||
|
} else {
|
||||||
|
R.drawable.bookmark
|
||||||
|
},
|
||||||
|
color = colorPalette.accent,
|
||||||
|
onClick = {
|
||||||
|
val bookmarkedAt =
|
||||||
|
if (artist?.bookmarkedAt == null) System.currentTimeMillis() else null
|
||||||
|
|
||||||
|
query {
|
||||||
|
artist
|
||||||
|
?.copy(bookmarkedAt = bookmarkedAt)
|
||||||
|
?.let(Database::update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.share_social,
|
||||||
|
color = colorPalette.text,
|
||||||
|
onClick = {
|
||||||
|
val sendIntent = Intent().apply {
|
||||||
|
action = Intent.ACTION_SEND
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_TEXT,
|
||||||
|
"https://music.youtube.com/channel/$browseId"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(Intent.createChooser(sendIntent, null))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
|
onTopIconButtonClick = pop,
|
||||||
|
tabIndex = tabIndex,
|
||||||
|
onTabChanged = onTabIndexChanged,
|
||||||
|
tabColumnContent = { Item ->
|
||||||
|
Item(0, "Overview", R.drawable.sparkles)
|
||||||
|
Item(1, "Songs", R.drawable.musical_notes)
|
||||||
|
Item(2, "Albums", R.drawable.disc)
|
||||||
|
Item(3, "Singles", R.drawable.disc)
|
||||||
|
Item(4, "Library", R.drawable.library)
|
||||||
|
},
|
||||||
|
) { currentTabIndex ->
|
||||||
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
|
when (currentTabIndex) {
|
||||||
|
0 -> ArtistOverview(
|
||||||
|
youtubeArtistPage = youtubeArtist,
|
||||||
|
thumbnailContent = thumbnailContent,
|
||||||
|
headerContent = headerContent,
|
||||||
|
onAlbumClick = { albumRoute(it) },
|
||||||
|
onViewAllSongsClick = { onTabIndexChanged(1) },
|
||||||
|
onViewAllAlbumsClick = { onTabIndexChanged(2) },
|
||||||
|
onViewAllSinglesClick = { onTabIndexChanged(3) },
|
||||||
|
)
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
ItemsPage(
|
||||||
|
stateSaver = InnertubeSongsPageSaver,
|
||||||
|
headerContent = headerContent,
|
||||||
|
itemsPageProvider = youtubeArtist?.let {
|
||||||
|
({ continuation ->
|
||||||
|
continuation?.let {
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = ContinuationBody(continuation = continuation),
|
||||||
|
fromMusicResponsiveListItemRenderer = Innertube.SongItem::from,
|
||||||
|
)
|
||||||
|
} ?: youtubeArtist
|
||||||
|
?.songsEndpoint
|
||||||
|
?.takeIf { it.browseId != null }
|
||||||
|
?.let { endpoint ->
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = BrowseBody(
|
||||||
|
browseId = endpoint.browseId!!,
|
||||||
|
params = endpoint.params,
|
||||||
|
),
|
||||||
|
fromMusicResponsiveListItemRenderer = Innertube.SongItem::from,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: Result.success(
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
items = youtubeArtist?.songs,
|
||||||
|
continuation = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
itemContent = { song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
NonQueuedMediaItemMenu(
|
||||||
|
onDismiss = menuState::hide,
|
||||||
|
mediaItem = song.asMediaItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlay(song.asMediaItem)
|
||||||
|
binder?.setupRadio(song.info?.endpoint)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
itemPlaceholderContent = {
|
||||||
|
SongItemPlaceholder(thumbnailSizeDp = thumbnailSizeDp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
2 -> {
|
||||||
|
val thumbnailSizeDp = 108.dp
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
ItemsPage(
|
||||||
|
stateSaver = InnertubeAlbumsPageSaver,
|
||||||
|
headerContent = headerContent,
|
||||||
|
emptyItemsText = "This artist didn't release any album",
|
||||||
|
itemsPageProvider = youtubeArtist?.let {
|
||||||
|
({ continuation ->
|
||||||
|
continuation?.let {
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = ContinuationBody(continuation = continuation),
|
||||||
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
|
)
|
||||||
|
} ?: youtubeArtist
|
||||||
|
?.albumsEndpoint
|
||||||
|
?.takeIf { it.browseId != null }
|
||||||
|
?.let { endpoint ->
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = BrowseBody(
|
||||||
|
browseId = endpoint.browseId!!,
|
||||||
|
params = endpoint.params,
|
||||||
|
),
|
||||||
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: Result.success(
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
items = youtubeArtist?.albums,
|
||||||
|
continuation = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
itemContent = { album ->
|
||||||
|
AlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { albumRoute(album.key) })
|
||||||
|
)
|
||||||
|
},
|
||||||
|
itemPlaceholderContent = {
|
||||||
|
AlbumItemPlaceholder(thumbnailSizeDp = thumbnailSizeDp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
3 -> {
|
||||||
|
val thumbnailSizeDp = 108.dp
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
ItemsPage(
|
||||||
|
stateSaver = InnertubeAlbumsPageSaver,
|
||||||
|
headerContent = headerContent,
|
||||||
|
emptyItemsText = "This artist didn't release any single",
|
||||||
|
itemsPageProvider = youtubeArtist?.let {
|
||||||
|
({ continuation ->
|
||||||
|
continuation?.let {
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = ContinuationBody(continuation = continuation),
|
||||||
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
|
)
|
||||||
|
} ?: youtubeArtist
|
||||||
|
?.singlesEndpoint
|
||||||
|
?.takeIf { it.browseId != null }
|
||||||
|
?.let { endpoint ->
|
||||||
|
Innertube.itemsPage(
|
||||||
|
body = BrowseBody(
|
||||||
|
browseId = endpoint.browseId!!,
|
||||||
|
params = endpoint.params,
|
||||||
|
),
|
||||||
|
fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: Result.success(
|
||||||
|
Innertube.ItemsPage(
|
||||||
|
items = youtubeArtist?.singles,
|
||||||
|
continuation = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
itemContent = { album ->
|
||||||
|
AlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { albumRoute(album.key) })
|
||||||
|
)
|
||||||
|
},
|
||||||
|
itemPlaceholderContent = {
|
||||||
|
AlbumItemPlaceholder(thumbnailSizeDp = thumbnailSizeDp)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
4 -> ArtistLocalSongs(
|
||||||
|
browseId = browseId,
|
||||||
|
headerContent = headerContent,
|
||||||
|
thumbnailContent = thumbnailContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.builtinplaylist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun BuiltInPlaylistScreen(builtInPlaylist: BuiltInPlaylist) {
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
|
val (tabIndex, onTabIndexChanged) = rememberSaveable {
|
||||||
|
mutableStateOf(when (builtInPlaylist) {
|
||||||
|
BuiltInPlaylist.Favorites -> 0
|
||||||
|
BuiltInPlaylist.Offline -> 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
|
globalRoutes()
|
||||||
|
|
||||||
|
host {
|
||||||
|
Scaffold(
|
||||||
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
|
onTopIconButtonClick = pop,
|
||||||
|
tabIndex = tabIndex,
|
||||||
|
onTabChanged = onTabIndexChanged,
|
||||||
|
tabColumnContent = { Item ->
|
||||||
|
Item(0, "Favorites", R.drawable.heart)
|
||||||
|
Item(1, "Offline", R.drawable.airplane)
|
||||||
|
}
|
||||||
|
) { currentTabIndex ->
|
||||||
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
|
when (currentTabIndex) {
|
||||||
|
0 -> BuiltInPlaylistSongs(builtInPlaylist = BuiltInPlaylist.Favorites)
|
||||||
|
1 -> BuiltInPlaylistSongs(builtInPlaylist = BuiltInPlaylist.Offline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.builtinplaylist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
|
||||||
|
val songs by produceSaveableState(
|
||||||
|
initialValue = emptyList(),
|
||||||
|
stateSaver = DetailedSongListSaver
|
||||||
|
) {
|
||||||
|
when (builtInPlaylist) {
|
||||||
|
BuiltInPlaylist.Favorites -> Database
|
||||||
|
.favorites()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
BuiltInPlaylist.Offline -> Database
|
||||||
|
.songsWithContentLength()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.map { songs ->
|
||||||
|
songs.filter { song ->
|
||||||
|
song.contentLength?.let {
|
||||||
|
binder?.cache?.isCached(song.id, 0, song.contentLength)
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val thumbnailSize = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
Box {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Header(
|
||||||
|
title = when (builtInPlaylist) {
|
||||||
|
BuiltInPlaylist.Favorites -> "Favorites"
|
||||||
|
BuiltInPlaylist.Offline -> "Offline"
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "Enqueue",
|
||||||
|
enabled = songs.isNotEmpty(),
|
||||||
|
onClick = {
|
||||||
|
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = songs,
|
||||||
|
key = { _, song -> song.id },
|
||||||
|
contentType = { _, song -> song },
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
thumbnailSizePx = thumbnailSize,
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
when (builtInPlaylist) {
|
||||||
|
BuiltInPlaylist.Favorites -> NonQueuedMediaItemMenu(
|
||||||
|
mediaItem = song.asMediaItem,
|
||||||
|
onDismiss = menuState::hide
|
||||||
|
)
|
||||||
|
|
||||||
|
BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(
|
||||||
|
song = song,
|
||||||
|
onDismiss = menuState::hide
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(
|
||||||
|
songs.map(DetailedSong::asMediaItem),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
iconId = R.drawable.shuffle,
|
||||||
|
onClick = {
|
||||||
|
if (songs.isNotEmpty()) {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(
|
||||||
|
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.home
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.AlbumSortBy
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
|
import it.vfsfitvnm.vimusic.models.Album
|
||||||
|
import it.vfsfitvnm.vimusic.savers.AlbumListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.albumSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.albumSortOrderKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun HomeAlbums(
|
||||||
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
onSearchClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableState(
|
||||||
|
initialValue = emptyList(),
|
||||||
|
stateSaver = AlbumListSaver,
|
||||||
|
sortBy, sortOrder,
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.albums(sortBy, sortOrder)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song * 2
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
|
)
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
Box {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Header(title = "Albums") {
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.calendar,
|
||||||
|
color = if (sortBy == AlbumSortBy.Year) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = AlbumSortBy.Year }
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.text,
|
||||||
|
color = if (sortBy == AlbumSortBy.Title) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = AlbumSortBy.Title }
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.time,
|
||||||
|
color = if (sortBy == AlbumSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = AlbumSortBy.DateAdded }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.arrow_up,
|
||||||
|
color = colorPalette.text,
|
||||||
|
onClick = { sortOrder = !sortOrder },
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = Album::id
|
||||||
|
) { album ->
|
||||||
|
AlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onAlbumClick(album) })
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
iconId = R.drawable.search,
|
||||||
|
onClick = onSearchClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.home
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.ArtistSortBy
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
|
import it.vfsfitvnm.vimusic.models.Artist
|
||||||
|
import it.vfsfitvnm.vimusic.savers.ArtistListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.ArtistItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.artistSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.artistSortOrderKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun HomeArtistList(
|
||||||
|
onArtistClick: (Artist) -> Unit,
|
||||||
|
onSearchClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableState(
|
||||||
|
initialValue = emptyList(),
|
||||||
|
stateSaver = ArtistListSaver,
|
||||||
|
sortBy, sortOrder,
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.artists(sortBy, sortOrder)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song * 2
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
|
)
|
||||||
|
|
||||||
|
val lazyGridState = rememberLazyGridState()
|
||||||
|
|
||||||
|
Box {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
state = lazyGridState,
|
||||||
|
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
space = Dimensions.itemsVerticalPadding * 2,
|
||||||
|
alignment = Alignment.CenterHorizontally
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0,
|
||||||
|
span = { GridItemSpan(maxLineSpan) }
|
||||||
|
) {
|
||||||
|
Header(title = "Artists") {
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.text,
|
||||||
|
color = if (sortBy == ArtistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = ArtistSortBy.Name }
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.time,
|
||||||
|
color = if (sortBy == ArtistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = ArtistSortBy.DateAdded }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.arrow_up,
|
||||||
|
color = colorPalette.text,
|
||||||
|
onClick = { sortOrder = !sortOrder },
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(items = items, key = Artist::id) { artist ->
|
||||||
|
ArtistItem(
|
||||||
|
artist = artist,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onArtistClick(artist) })
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyGridState = lazyGridState,
|
||||||
|
iconId = R.drawable.search,
|
||||||
|
onClick = onSearchClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.home
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
|
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.PlaylistItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun HomePlaylists(
|
||||||
|
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
|
||||||
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
|
onSearchClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
var isCreatingANewPlaylist by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreatingANewPlaylist) {
|
||||||
|
TextFieldDialog(
|
||||||
|
hintText = "Enter the playlist name",
|
||||||
|
onDismiss = {
|
||||||
|
isCreatingANewPlaylist = false
|
||||||
|
},
|
||||||
|
onDone = { text ->
|
||||||
|
query {
|
||||||
|
Database.insert(Playlist(name = text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableState(
|
||||||
|
initialValue = emptyList(),
|
||||||
|
stateSaver = PlaylistPreviewListSaver,
|
||||||
|
sortBy, sortOrder,
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.playlistPreviews(sortBy, sortOrder)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
|
)
|
||||||
|
|
||||||
|
val thumbnailSizeDp = 108.dp
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
val lazyGridState = rememberLazyGridState()
|
||||||
|
|
||||||
|
Box {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
state = lazyGridState,
|
||||||
|
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
space = Dimensions.itemsVerticalPadding * 2,
|
||||||
|
alignment = Alignment.CenterHorizontally
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
) {
|
||||||
|
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Header(title = "Playlists") {
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "New playlist",
|
||||||
|
onClick = { isCreatingANewPlaylist = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.medical,
|
||||||
|
color = if (sortBy == PlaylistSortBy.SongCount) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = PlaylistSortBy.SongCount }
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.text,
|
||||||
|
color = if (sortBy == PlaylistSortBy.Name) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = PlaylistSortBy.Name }
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.time,
|
||||||
|
color = if (sortBy == PlaylistSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = PlaylistSortBy.DateAdded }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.arrow_up,
|
||||||
|
color = colorPalette.text,
|
||||||
|
onClick = { sortOrder = !sortOrder },
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "favorites") {
|
||||||
|
PlaylistItem(
|
||||||
|
icon = R.drawable.heart,
|
||||||
|
colorTint = colorPalette.red,
|
||||||
|
name = "Favorites",
|
||||||
|
songCount = null,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) })
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "offline") {
|
||||||
|
PlaylistItem(
|
||||||
|
icon = R.drawable.airplane,
|
||||||
|
colorTint = colorPalette.blue,
|
||||||
|
name = "Offline",
|
||||||
|
songCount = null,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) })
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(items = items, key = { it.playlist.id }) { playlistPreview ->
|
||||||
|
PlaylistItem(
|
||||||
|
playlist = playlistPreview,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onPlaylistClick(playlistPreview.playlist) })
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyGridState = lazyGridState,
|
||||||
|
iconId = R.drawable.search,
|
||||||
|
onClick = onSearchClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.home
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.route.defaultStacking
|
||||||
|
import it.vfsfitvnm.route.defaultStill
|
||||||
|
import it.vfsfitvnm.route.defaultUnstacking
|
||||||
|
import it.vfsfitvnm.route.isStacking
|
||||||
|
import it.vfsfitvnm.route.isUnknown
|
||||||
|
import it.vfsfitvnm.route.isUnstacking
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.SearchQuery
|
||||||
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.builtinplaylist.BuiltInPlaylistScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.searchResultRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.searchRoute
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResultScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.settings.SettingsScreen
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.settingsRoute
|
||||||
|
import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
|
RouteHandler(
|
||||||
|
listenToGlobalEmitter = true,
|
||||||
|
transitionSpec = {
|
||||||
|
when {
|
||||||
|
isStacking -> defaultStacking
|
||||||
|
isUnstacking -> defaultUnstacking
|
||||||
|
isUnknown -> when {
|
||||||
|
initialState.route == searchRoute && targetState.route == searchResultRoute -> defaultStacking
|
||||||
|
initialState.route == searchResultRoute && targetState.route == searchRoute -> defaultUnstacking
|
||||||
|
else -> defaultStill
|
||||||
|
}
|
||||||
|
else -> defaultStill
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
globalRoutes()
|
||||||
|
|
||||||
|
settingsRoute {
|
||||||
|
SettingsScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
localPlaylistRoute { playlistId ->
|
||||||
|
LocalPlaylistScreen(
|
||||||
|
playlistId = playlistId ?: error("playlistId cannot be null")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
builtInPlaylistRoute { builtInPlaylist ->
|
||||||
|
BuiltInPlaylistScreen(
|
||||||
|
builtInPlaylist = builtInPlaylist
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResultRoute { query ->
|
||||||
|
SearchResultScreen(
|
||||||
|
query = query,
|
||||||
|
onSearchAgain = {
|
||||||
|
searchRoute(query)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchRoute { initialTextInput ->
|
||||||
|
SearchScreen(
|
||||||
|
initialTextInput = initialTextInput,
|
||||||
|
onSearch = { query ->
|
||||||
|
pop()
|
||||||
|
searchResultRoute(query)
|
||||||
|
|
||||||
|
query {
|
||||||
|
Database.insert(SearchQuery(query = query))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onViewPlaylist = onPlaylistUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
host {
|
||||||
|
val (tabIndex, onTabChanged) = rememberPreference(
|
||||||
|
homeScreenTabIndexKey,
|
||||||
|
defaultValue = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topIconButtonId = R.drawable.equalizer,
|
||||||
|
onTopIconButtonClick = { settingsRoute() },
|
||||||
|
tabIndex = tabIndex,
|
||||||
|
onTabChanged = onTabChanged,
|
||||||
|
tabColumnContent = { Item ->
|
||||||
|
Item(0, "Quick picks", R.drawable.sparkles)
|
||||||
|
Item(1, "Songs", R.drawable.musical_notes)
|
||||||
|
Item(2, "Playlists", R.drawable.playlist)
|
||||||
|
Item(3, "Artists", R.drawable.person)
|
||||||
|
Item(4, "Albums", R.drawable.disc)
|
||||||
|
},
|
||||||
|
) { currentTabIndex ->
|
||||||
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
|
when (currentTabIndex) {
|
||||||
|
0 -> QuickPicks(
|
||||||
|
onAlbumClick = { albumRoute(it) },
|
||||||
|
onArtistClick = { artistRoute(it) },
|
||||||
|
onPlaylistClick = { playlistRoute(it) },
|
||||||
|
onSearchClick = { searchRoute("") }
|
||||||
|
)
|
||||||
|
1 -> HomeSongs(
|
||||||
|
onSearchClick = { searchRoute("") }
|
||||||
|
)
|
||||||
|
2 -> HomePlaylists(
|
||||||
|
onBuiltInPlaylist = { builtInPlaylistRoute(it) },
|
||||||
|
onPlaylistClick = { localPlaylistRoute(it.id) },
|
||||||
|
onSearchClick = { searchRoute("") }
|
||||||
|
)
|
||||||
|
3 -> HomeArtistList(
|
||||||
|
onArtistClick = { artistRoute(it.id) },
|
||||||
|
onSearchClick = { searchRoute("") }
|
||||||
|
)
|
||||||
|
4 -> HomeAlbums(
|
||||||
|
onAlbumClick = { albumRoute(it.id) },
|
||||||
|
onSearchClick = { searchRoute("") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.home
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.overlay
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun HomeSongs(
|
||||||
|
onSearchClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
|
||||||
|
var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
|
||||||
|
|
||||||
|
val items by produceSaveableState(
|
||||||
|
initialValue = emptyList(),
|
||||||
|
stateSaver = DetailedSongListSaver,
|
||||||
|
sortBy, sortOrder,
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.songs(sortBy, sortOrder)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
|
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
|
)
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Header(title = "Songs") {
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.trending,
|
||||||
|
color = if (sortBy == SongSortBy.PlayTime) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = SongSortBy.PlayTime }
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.text,
|
||||||
|
color = if (sortBy == SongSortBy.Title) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = SongSortBy.Title }
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.time,
|
||||||
|
color = if (sortBy == SongSortBy.DateAdded) colorPalette.text else colorPalette.textDisabled,
|
||||||
|
onClick = { sortBy = SongSortBy.DateAdded }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.arrow_up,
|
||||||
|
color = colorPalette.text,
|
||||||
|
onClick = { sortOrder = !sortOrder },
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = items,
|
||||||
|
key = { _, song -> song.id }
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
onThumbnailContent = if (sortBy == SongSortBy.PlayTime) ({
|
||||||
|
BasicText(
|
||||||
|
text = song.formattedTotalPlayTime,
|
||||||
|
style = typography.xxs.semiBold.center.color(colorPalette.onOverlay),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
colors = listOf(Color.Transparent, colorPalette.overlay)
|
||||||
|
),
|
||||||
|
shape = thumbnailShape
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
|
}) else null,
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
InHistoryMediaItemMenu(
|
||||||
|
song = song,
|
||||||
|
onDismiss = menuState::hide
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(
|
||||||
|
items.map(DetailedSong::asMediaItem),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
iconId = R.drawable.search,
|
||||||
|
onClick = onSearchClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,371 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.home
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.InnertubeRelatedPageSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.resultSaver
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.ArtistItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.ArtistItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.PlaylistItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.PlaylistItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.SnapLayoutInfoProvider
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.NextBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.relatedPage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun QuickPicks(
|
||||||
|
onAlbumClick: (String) -> Unit,
|
||||||
|
onArtistClick: (String) -> Unit,
|
||||||
|
onPlaylistClick: (String) -> Unit,
|
||||||
|
onSearchClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
val windowInsets = LocalPlayerAwareWindowInsets.current
|
||||||
|
|
||||||
|
val trending by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(DetailedSongSaver),
|
||||||
|
) {
|
||||||
|
Database.trending()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.filterNotNull()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val relatedPageResult by produceSaveableOneShotState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = resultSaver(nullableSaver(InnertubeRelatedPageSaver)),
|
||||||
|
trending?.id
|
||||||
|
) {
|
||||||
|
value = Innertube.relatedPage(NextBody(videoId = (trending?.id ?: "J7p4bzqLvCw")))
|
||||||
|
}
|
||||||
|
|
||||||
|
val songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||||
|
val albumThumbnailSizeDp = 108.dp
|
||||||
|
val albumThumbnailSizePx = albumThumbnailSizeDp.px
|
||||||
|
val artistThumbnailSizeDp = 92.dp
|
||||||
|
val artistThumbnailSizePx = artistThumbnailSizeDp.px
|
||||||
|
val playlistThumbnailSizeDp = 108.dp
|
||||||
|
val playlistThumbnailSizePx = playlistThumbnailSizeDp.px
|
||||||
|
|
||||||
|
val quickPicksLazyGridItemWidthFactor = 0.9f
|
||||||
|
val quickPicksLazyGridState = rememberLazyGridState()
|
||||||
|
val snapLayoutInfoProvider = remember(quickPicksLazyGridState) {
|
||||||
|
SnapLayoutInfoProvider(
|
||||||
|
lazyGridState = quickPicksLazyGridState,
|
||||||
|
positionInLayout = {layoutSize, itemSize ->
|
||||||
|
(layoutSize * quickPicksLazyGridItemWidthFactor / 2f - itemSize / 2f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
val endPaddingValues = windowInsets.only(WindowInsetsSides.End).asPaddingValues()
|
||||||
|
|
||||||
|
val sectionTextModifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(top = 24.dp, bottom = 8.dp)
|
||||||
|
.padding(endPaddingValues)
|
||||||
|
|
||||||
|
BoxWithConstraints {
|
||||||
|
val itemInHorizontalGridWidth = maxWidth * quickPicksLazyGridItemWidthFactor
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(windowInsets.only(WindowInsetsSides.Vertical).asPaddingValues())
|
||||||
|
) {
|
||||||
|
Header(
|
||||||
|
title = "Quick picks",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(endPaddingValues)
|
||||||
|
)
|
||||||
|
|
||||||
|
relatedPageResult?.getOrNull()?.let { related ->
|
||||||
|
LazyHorizontalGrid(
|
||||||
|
state = quickPicksLazyGridState,
|
||||||
|
rows = GridCells.Fixed(4),
|
||||||
|
flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider),
|
||||||
|
contentPadding = endPaddingValues,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height((songThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2) * 4)
|
||||||
|
) {
|
||||||
|
trending?.let { song ->
|
||||||
|
item {
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizePx = songThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
trailingContent = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.star),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.accent),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
NonQueuedMediaItemMenu(
|
||||||
|
onDismiss = menuState::hide,
|
||||||
|
mediaItem = song.asMediaItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val mediaItem = song.asMediaItem
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlay(mediaItem)
|
||||||
|
binder?.setupRadio(
|
||||||
|
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.animateItemPlacement()
|
||||||
|
.width(itemInHorizontalGridWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items(
|
||||||
|
items = related.songs?.dropLast(if (trending == null) 0 else 1) ?: emptyList(),
|
||||||
|
key = Innertube.SongItem::key
|
||||||
|
) { song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizePx = songThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
NonQueuedMediaItemMenu(
|
||||||
|
onDismiss = menuState::hide,
|
||||||
|
mediaItem = song.asMediaItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
val mediaItem = song.asMediaItem
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlay(mediaItem)
|
||||||
|
binder?.setupRadio(
|
||||||
|
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.animateItemPlacement()
|
||||||
|
.width(itemInHorizontalGridWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
related.albums?.let { albums ->
|
||||||
|
BasicText(
|
||||||
|
text = "Related albums",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(contentPadding = endPaddingValues) {
|
||||||
|
items(
|
||||||
|
items = albums,
|
||||||
|
key = Innertube.AlbumItem::key
|
||||||
|
) { album ->
|
||||||
|
AlbumItem(
|
||||||
|
album = album,
|
||||||
|
thumbnailSizePx = albumThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onAlbumClick(album.key) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
related.artists?.let { artists ->
|
||||||
|
BasicText(
|
||||||
|
text = "Similar artists",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = sectionTextModifier
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(contentPadding = endPaddingValues) {
|
||||||
|
items(
|
||||||
|
items = artists,
|
||||||
|
key = Innertube.ArtistItem::key,
|
||||||
|
) { artist ->
|
||||||
|
ArtistItem(
|
||||||
|
artist = artist,
|
||||||
|
thumbnailSizePx = artistThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = artistThumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onArtistClick(artist.key) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
related.playlists?.let { playlists ->
|
||||||
|
BasicText(
|
||||||
|
text = "Playlists you might like",
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(top = 24.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyRow(contentPadding = endPaddingValues) {
|
||||||
|
items(
|
||||||
|
items = playlists,
|
||||||
|
key = Innertube.PlaylistItem::key,
|
||||||
|
) { playlist ->
|
||||||
|
PlaylistItem(
|
||||||
|
playlist = playlist,
|
||||||
|
thumbnailSizePx = playlistThumbnailSizePx,
|
||||||
|
thumbnailSizeDp = playlistThumbnailSizeDp,
|
||||||
|
alternative = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = { onPlaylistClick(playlist.key) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: relatedPageResult?.exceptionOrNull()?.let {
|
||||||
|
BasicText(
|
||||||
|
text = "An error has occurred",
|
||||||
|
style = typography.s.secondary.center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
)
|
||||||
|
} ?: ShimmerHost {
|
||||||
|
repeat(4) {
|
||||||
|
SongItemPlaceholder(
|
||||||
|
thumbnailSizeDp = songThumbnailSizeDp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
repeat(2) {
|
||||||
|
AlbumItemPlaceholder(
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
alternative = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
repeat(2) {
|
||||||
|
ArtistItemPlaceholder(
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
alternative = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextPlaceholder(modifier = sectionTextModifier)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
repeat(2) {
|
||||||
|
PlaylistItemPlaceholder(
|
||||||
|
thumbnailSizeDp = albumThumbnailSizeDp,
|
||||||
|
alternative = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
scrollState = scrollState,
|
||||||
|
iconId = R.drawable.search,
|
||||||
|
onClick = onSearchClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.localplaylist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun LocalPlaylistScreen(playlistId: Long) {
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
|
globalRoutes()
|
||||||
|
|
||||||
|
host {
|
||||||
|
Scaffold(
|
||||||
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
|
onTopIconButtonClick = pop,
|
||||||
|
tabIndex = 0,
|
||||||
|
onTabChanged = { },
|
||||||
|
tabColumnContent = { Item ->
|
||||||
|
Item(0, "Songs", R.drawable.musical_notes)
|
||||||
|
}
|
||||||
|
) { currentTabIndex ->
|
||||||
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
|
LocalPlaylistSongs(
|
||||||
|
playlistId = playlistId,
|
||||||
|
onDelete = pop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,302 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.screens.localplaylist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.unit.dp
|
||||||
|
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||||
|
import it.vfsfitvnm.reordering.animateItemPlacement
|
||||||
|
import it.vfsfitvnm.reordering.draggedItem
|
||||||
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
|
import it.vfsfitvnm.reordering.reorder
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
||||||
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||||
|
import it.vfsfitvnm.vimusic.transaction
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||||
|
import it.vfsfitvnm.vimusic.ui.items.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.completed
|
||||||
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||||
|
import it.vfsfitvnm.youtubemusic.requests.playlistPage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun LocalPlaylistSongs(
|
||||||
|
playlistId: Long,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val menuState = LocalMenuState.current
|
||||||
|
|
||||||
|
val playlistWithSongs by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = nullableSaver(PlaylistWithSongsSaver)
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.playlistWithSongs(playlistId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
val reorderingState = rememberReorderingState(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
key = playlistWithSongs?.songs ?: emptyList<Any>(),
|
||||||
|
onDragEnd = { fromIndex, toIndex ->
|
||||||
|
query {
|
||||||
|
Database.move(playlistId, fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraItemCount = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var isRenaming by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRenaming) {
|
||||||
|
TextFieldDialog(
|
||||||
|
hintText = "Enter the playlist name",
|
||||||
|
initialTextInput = playlistWithSongs?.playlist?.name ?: "",
|
||||||
|
onDismiss = { isRenaming = false },
|
||||||
|
onDone = { text ->
|
||||||
|
query {
|
||||||
|
playlistWithSongs?.playlist?.copy(name = text)?.let(Database::update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDeleting by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeleting) {
|
||||||
|
ConfirmationDialog(
|
||||||
|
text = "Do you really want to delete this playlist?",
|
||||||
|
onDismiss = { isDeleting = false },
|
||||||
|
onConfirm = {
|
||||||
|
query {
|
||||||
|
playlistWithSongs?.playlist?.let(Database::delete)
|
||||||
|
}
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
|
val rippleIndication = rememberRipple(bounded = false)
|
||||||
|
|
||||||
|
Box {
|
||||||
|
ReorderingLazyColumn(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
contentPadding = LocalPlayerAwareWindowInsets.current
|
||||||
|
.only(WindowInsetsSides.Vertical + WindowInsetsSides.End).asPaddingValues(),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Header(
|
||||||
|
title = playlistWithSongs?.playlist?.name ?: "Unknown",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
SecondaryTextButton(
|
||||||
|
text = "Enqueue",
|
||||||
|
enabled = playlistWithSongs?.songs?.isNotEmpty() == true,
|
||||||
|
onClick = {
|
||||||
|
playlistWithSongs?.songs
|
||||||
|
?.map(DetailedSong::asMediaItem)
|
||||||
|
?.let { mediaItems ->
|
||||||
|
binder?.player?.enqueue(mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
HeaderIconButton(
|
||||||
|
icon = R.drawable.ellipsis_horizontal,
|
||||||
|
color = colorPalette.text,
|
||||||
|
onClick = {
|
||||||
|
menuState.display {
|
||||||
|
Menu {
|
||||||
|
playlistWithSongs?.playlist?.browseId?.let { browseId ->
|
||||||
|
MenuEntry(
|
||||||
|
icon = R.drawable.sync,
|
||||||
|
text = "Sync",
|
||||||
|
onClick = {
|
||||||
|
menuState.hide()
|
||||||
|
transaction {
|
||||||
|
runBlocking(Dispatchers.IO) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Innertube.playlistPage(BrowseBody(browseId = browseId))
|
||||||
|
?.completed()
|
||||||
|
}
|
||||||
|
}?.getOrNull()?.let { remotePlaylist ->
|
||||||
|
Database.clearPlaylist(playlistId)
|
||||||
|
|
||||||
|
remotePlaylist.songsPage
|
||||||
|
?.items
|
||||||
|
?.map(Innertube.SongItem::asMediaItem)
|
||||||
|
?.onEach(Database::insert)
|
||||||
|
?.mapIndexed { position, mediaItem ->
|
||||||
|
SongPlaylistMap(
|
||||||
|
songId = mediaItem.mediaId,
|
||||||
|
playlistId = playlistId,
|
||||||
|
position = position
|
||||||
|
)
|
||||||
|
}?.let(Database::insertSongPlaylistMaps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuEntry(
|
||||||
|
icon = R.drawable.pencil,
|
||||||
|
text = "Rename",
|
||||||
|
onClick = {
|
||||||
|
menuState.hide()
|
||||||
|
isRenaming = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
MenuEntry(
|
||||||
|
icon = R.drawable.trash,
|
||||||
|
text = "Delete",
|
||||||
|
onClick = {
|
||||||
|
menuState.hide()
|
||||||
|
isDeleting = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = playlistWithSongs?.songs ?: emptyList(),
|
||||||
|
key = { _, song -> song.id },
|
||||||
|
contentType = { _, song -> song },
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSizePx = thumbnailSizePx,
|
||||||
|
thumbnailSizeDp = thumbnailSizeDp,
|
||||||
|
trailingContent = {
|
||||||
|
IconButton(
|
||||||
|
icon = R.drawable.reorder,
|
||||||
|
color = colorPalette.textDisabled,
|
||||||
|
indication = rippleIndication,
|
||||||
|
onClick = {},
|
||||||
|
modifier = Modifier
|
||||||
|
.reorder(reorderingState = reorderingState, index = index)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = {
|
||||||
|
menuState.display {
|
||||||
|
InPlaylistMediaItemMenu(
|
||||||
|
playlistId = playlistId,
|
||||||
|
positionInPlaylist = index,
|
||||||
|
song = song,
|
||||||
|
onDismiss = menuState::hide
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
playlistWithSongs?.songs
|
||||||
|
?.map(DetailedSong::asMediaItem)
|
||||||
|
?.let { mediaItems ->
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.animateItemPlacement(reorderingState = reorderingState)
|
||||||
|
.draggedItem(reorderingState = reorderingState, index = index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionsContainerWithScrollToTop(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
iconId = R.drawable.shuffle,
|
||||||
|
visible = !reorderingState.isDragging,
|
||||||
|
onClick = {
|
||||||
|
playlistWithSongs?.songs?.let { songs ->
|
||||||
|
if (songs.isNotEmpty()) {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(
|
||||||
|
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.views.player
|
package it.vfsfitvnm.vimusic.ui.screens.player
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
@ -21,10 +21,10 @@ import androidx.compose.foundation.layout.width
|
||||||
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.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
import androidx.compose.runtime.setValue
|
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
|
||||||
|
@ -41,16 +41,19 @@ import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.models.Song
|
import it.vfsfitvnm.vimusic.models.Song
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.ui.components.SeekBar
|
import it.vfsfitvnm.vimusic.ui.components.SeekBar
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.IconButton
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon
|
import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon
|
||||||
import it.vfsfitvnm.vimusic.utils.bold
|
import it.vfsfitvnm.vimusic.utils.bold
|
||||||
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
import it.vfsfitvnm.vimusic.utils.forceSeekToNext
|
||||||
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
|
import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberRepeatMode
|
import it.vfsfitvnm.vimusic.utils.rememberRepeatMode
|
||||||
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 kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Controls(
|
fun Controls(
|
||||||
|
@ -73,9 +76,17 @@ fun Controls(
|
||||||
mutableStateOf<Long?>(null)
|
mutableStateOf<Long?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val likedAt by remember(mediaId) {
|
val likedAt by produceSaveableState<Long?>(
|
||||||
Database.likedAt(mediaId).distinctUntilChanged()
|
initialValue = null,
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
stateSaver = autoSaver(),
|
||||||
|
mediaId
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.likedAt(mediaId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
val shouldBePlayingTransition = updateTransition(shouldBePlaying, label = "shouldBePlaying")
|
val shouldBePlayingTransition = updateTransition(shouldBePlaying, label = "shouldBePlaying")
|
||||||
|
|
||||||
|
@ -150,7 +161,8 @@ fun Controls(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = DateUtils.formatElapsedTime((scrubbingPosition ?: position) / 1000),
|
text = DateUtils.formatElapsedTime((scrubbingPosition ?: position) / 1000)
|
||||||
|
.removePrefix("0"),
|
||||||
style = typography.xxs.semiBold,
|
style = typography.xxs.semiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
@ -158,7 +170,7 @@ fun Controls(
|
||||||
|
|
||||||
if (duration != C.TIME_UNSET) {
|
if (duration != C.TIME_UNSET) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = DateUtils.formatElapsedTime(duration / 1000),
|
text = DateUtils.formatElapsedTime(duration / 1000).removePrefix("0"),
|
||||||
style = typography.xxs.semiBold,
|
style = typography.xxs.semiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
@ -176,31 +188,35 @@ fun Controls(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Image(
|
IconButton(
|
||||||
painter = painterResource(if (likedAt == null) R.drawable.heart_outline else R.drawable.heart),
|
icon = if (likedAt == null) R.drawable.heart_outline else R.drawable.heart,
|
||||||
contentDescription = null,
|
color = colorPalette.favoritesIcon,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.favoritesIcon),
|
onClick = {
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
val currentMediaItem = binder.player.currentMediaItem
|
val currentMediaItem = binder.player.currentMediaItem
|
||||||
query {
|
query {
|
||||||
if (Database.like(mediaId, if (likedAt == null) System.currentTimeMillis() else null) == 0) {
|
if (Database.like(
|
||||||
currentMediaItem?.takeIf { it.mediaId == mediaId }?.let {
|
mediaId,
|
||||||
|
if (likedAt == null) System.currentTimeMillis() else null
|
||||||
|
) == 0
|
||||||
|
) {
|
||||||
|
currentMediaItem
|
||||||
|
?.takeIf { it.mediaId == mediaId }
|
||||||
|
?.let {
|
||||||
Database.insert(currentMediaItem, Song::toggleLike)
|
Database.insert(currentMediaItem, Song::toggleLike)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Image(
|
IconButton(
|
||||||
painter = painterResource(R.drawable.play_skip_back),
|
icon = R.drawable.play_skip_back,
|
||||||
contentDescription = null,
|
color = colorPalette.text,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
onClick = binder.player::forceSeekToPrevious,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = binder.player::forceSeekToPrevious)
|
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
|
@ -241,33 +257,29 @@ fun Controls(
|
||||||
.width(8.dp)
|
.width(8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Image(
|
IconButton(
|
||||||
painter = painterResource(R.drawable.play_skip_forward),
|
icon = R.drawable.play_skip_forward,
|
||||||
contentDescription = null,
|
color = colorPalette.text,
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
onClick = binder.player::forceSeekToNext,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = binder.player::forceSeekToNext)
|
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Image(
|
IconButton(
|
||||||
painter = painterResource(R.drawable.infinite),
|
icon = R.drawable.infinite,
|
||||||
contentDescription = null,
|
color = if (repeatMode == Player.REPEAT_MODE_ONE) {
|
||||||
colorFilter = ColorFilter.tint(
|
|
||||||
if (repeatMode == Player.REPEAT_MODE_ONE) {
|
|
||||||
colorPalette.text
|
colorPalette.text
|
||||||
} else {
|
} else {
|
||||||
colorPalette.textDisabled
|
colorPalette.textDisabled
|
||||||
}
|
},
|
||||||
),
|
onClick = {
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
binder.player.repeatMode = when (binder.player.repeatMode) {
|
binder.player.repeatMode = when (binder.player.repeatMode) {
|
||||||
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
|
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
|
||||||
else -> Player.REPEAT_MODE_ONE
|
else -> Player.REPEAT_MODE_ONE
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
)
|
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue