Redesign AlbumScreen (#172)

This commit is contained in:
vfsfitvnm 2022-09-24 20:18:43 +02:00
parent a54b795666
commit f463f82f72
14 changed files with 1066 additions and 500 deletions

View file

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

View file

@ -32,6 +32,7 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.AlbumWithSongs
import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
@ -45,6 +46,7 @@ import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.models.SongAlbumMap
import it.vfsfitvnm.vimusic.models.SongArtistMap
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.models.SortedSongAlbumMap
import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@ -144,8 +146,9 @@ interface Database {
@Query("SELECT * FROM Artist WHERE id = :id")
fun artist(id: String): Flow<Artist?>
@Transaction
@Query("SELECT * FROM Album WHERE id = :id")
fun album(id: String): Flow<Album?>
fun albumWithSongs(id: String): Flow<AlbumWithSongs?>
@Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id")
fun incrementTotalPlayTimeMs(id: String, addition: Long)
@ -190,11 +193,6 @@ interface Database {
@RewriteQueriesToDropUnusedColumns
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")
fun format(songId: String): Flow<Format>
@ -361,9 +359,10 @@ interface Database {
Format::class,
],
views = [
SortedSongPlaylistMap::class
SortedSongPlaylistMap::class,
SortedSongAlbumMap::class
],
version = 17,
version = 18,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
@ -379,6 +378,7 @@ interface Database {
AutoMigration(from = 13, to = 14),
AutoMigration(from = 15, to = 16),
AutoMigration(from = 16, to = 17),
AutoMigration(from = 17, to = 18),
],
)
@TypeConverters(Converters::class)

View file

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

View file

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

View file

@ -1,6 +1,8 @@
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
@ -11,12 +13,15 @@ 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.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.medium
import kotlin.random.Random
@Composable
fun Header(
@ -69,3 +74,32 @@ fun Header(
)
}
}
@Composable
fun HeaderPlaceholder(
modifier: Modifier = Modifier,
) {
val (colorPalette, typography) = LocalAppearance.current
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.Center,
modifier = modifier
.padding(horizontal = 16.dp)
.height(128.dp)
.fillMaxWidth()
) {
Box(
modifier = Modifier
.background(colorPalette.shimmer)
.fillMaxWidth(remember { 0.25f + Random.nextFloat() * 0.5f })
) {
BasicText(
text = "",
style = typography.xxl.medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}

View file

@ -112,16 +112,11 @@ fun Scaffold(
}
}
@SuppressLint("ModifierParameter")
@ExperimentalAnimationApi
@Composable
fun SimpleScaffold(
topIconButtonId: Int,
onTopIconButtonClick: () -> Unit,
title: String = "",
primaryIconButtonId: Int? = null,
primaryIconButtonEnabled: Boolean = true,
onPrimaryIconButtonClick: () -> Unit = {},
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
@ -132,13 +127,9 @@ fun SimpleScaffold(
.background(colorPalette.background0)
.fillMaxSize()
) {
VerticalTitleBar(
VerticalBar(
topIconButtonId = topIconButtonId,
onTopIconButtonClick = onTopIconButtonClick,
title = title,
primaryIconButtonId = primaryIconButtonId,
primaryIconButtonEnabled = primaryIconButtonEnabled,
onPrimaryIconButtonClick = onPrimaryIconButtonClick,
modifier = Modifier
.padding(LocalPlayerAwarePaddingValues.current)
)

View file

@ -24,6 +24,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.ui.components.TabColumn
import it.vfsfitvnm.vimusic.ui.components.vertical
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.semiBold
@ -35,8 +36,6 @@ fun VerticalBar(
tabIndex: Int,
onTabChanged: (Int) -> Unit,
tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit,
// primaryIconButtonId: Int? = null,
// onPrimaryIconButtonClick: () -> Unit = {},
modifier: Modifier = Modifier
) {
val (colorPalette, typography) = LocalAppearance.current
@ -44,6 +43,7 @@ fun VerticalBar(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
// .width(Dimensions.verticalBarWidth)
.padding(vertical = 16.dp)
) {
// Box(
@ -68,7 +68,7 @@ fun VerticalBar(
Spacer(
modifier = Modifier
.width(64.dp)
.width(Dimensions.verticalBarWidth)
.height(32.dp)
)
@ -110,77 +110,28 @@ fun VerticalBar(
@SuppressLint("ModifierParameter")
@Composable
fun VerticalTitleBar(
fun VerticalBar(
topIconButtonId: Int,
onTopIconButtonClick: () -> Unit,
title: String,
primaryIconButtonId: Int? = null,
primaryIconButtonEnabled: Boolean = true,
onPrimaryIconButtonClick: () -> Unit = {},
modifier: Modifier = Modifier
) {
val (colorPalette, typography) = LocalAppearance.current
val (colorPalette) = LocalAppearance.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.width(Dimensions.verticalBarWidth)
.padding(vertical = 16.dp)
) {
Box(
Image(
painter = painterResource(topIconButtonId),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = onTopIconButtonClick)
.background(color = colorPalette.background1)
.size(48.dp)
) {
Image(
painter = painterResource(topIconButtonId),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
modifier = Modifier
.align(Alignment.Center)
.size(22.dp)
)
}
Spacer(
modifier = Modifier
.width(78.dp)
.height(32.dp)
.padding(all = 12.dp)
.size(22.dp)
)
BasicText(
text = title,
style = typography.m.semiBold,
modifier = Modifier
.vertical()
.rotate(-90f)
.padding(horizontal = 16.dp)
)
Spacer(
modifier = Modifier
.weight(1f)
)
primaryIconButtonId?.let {
Box(
modifier = Modifier
.offset(x = 8.dp)
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = primaryIconButtonEnabled, onClick = onPrimaryIconButtonClick)
.background(colorPalette.background1)
.size(62.dp)
) {
Image(
painter = painterResource(primaryIconButtonId),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.align(Alignment.Center)
.size(20.dp)
)
}
}
}
}

View file

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

View file

@ -8,6 +8,7 @@ import it.vfsfitvnm.route.Route0
import it.vfsfitvnm.route.Route1
import it.vfsfitvnm.route.RouteHandlerScope
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen
val albumRoute = Route1<String?>("albumRoute")
val artistRoute = Route1<String?>("artistRoute")

View file

@ -0,0 +1,27 @@
package it.vfsfitvnm.vimusic.ui.screens.album
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.themed.SimpleScaffold
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
@OptIn(ExperimentalFoundationApi::class)
@ExperimentalAnimationApi
@Composable
fun AlbumScreen(browseId: String) {
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
SimpleScaffold(
topIconButtonId = R.drawable.chevron_back,
onTopIconButtonClick = pop,
) {
AlbumSongList(browseId = browseId)
}
}
}
}

View file

@ -0,0 +1,276 @@
package it.vfsfitvnm.vimusic.ui.screens.album
import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.thumbnail
@ExperimentalAnimationApi
@ExperimentalFoundationApi
@Composable
fun AlbumSongList(
browseId: String,
viewModel: AlbumViewModel = viewModel(
key = browseId,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return AlbumViewModel(browseId) as T
}
}
)
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val context = LocalContext.current
BoxWithConstraints {
val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
viewModel.result?.getOrNull()?.let { albumWithSongs ->
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
) {
item(
key = "header",
contentType = 0
) {
Column {
Header(title = albumWithSongs.album.title ?: "Unknown") {
if (albumWithSongs.songs.isNotEmpty()) {
BasicText(
text = "Enqueue",
style = typography.xxs.medium,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
binder?.player?.enqueue(
albumWithSongs.songs.map(DetailedSong::asMediaItem)
)
}
.background(colorPalette.background2)
.padding(all = 8.dp)
.padding(horizontal = 8.dp)
)
}
Spacer(
modifier = Modifier
.weight(1f)
)
Image(
painter = painterResource(R.drawable.share_social),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
albumWithSongs.album.shareUrl?.let { url ->
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)
}
context.startActivity(
Intent.createChooser(
sendIntent,
null
)
)
}
}
.padding(all = 4.dp)
.size(18.dp)
)
}
AsyncImage(
model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(all = 16.dp)
.clip(thumbnailShape)
.size(thumbnailSizeDp)
)
}
}
itemsIndexed(
items = albumWithSongs.songs,
key = { _, song -> song.id }
) { index, song ->
SongItem(
title = song.title,
authors = song.artistsText ?: albumWithSongs.album.authorsText,
durationText = song.durationText,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
albumWithSongs.songs.map(DetailedSong::asMediaItem),
index
)
},
startContent = {
BasicText(
text = "${index + 1}",
style = typography.m.secondary.secondary.center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.width(Dimensions.thumbnails.song)
)
},
menuContent = {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
)
}
}
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(all = 16.dp)
.padding(LocalPlayerAwarePaddingValues.current)
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = albumWithSongs.songs.isNotEmpty()) {
binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(
albumWithSongs.songs
.shuffled()
.map(DetailedSong::asMediaItem)
)
}
.background(colorPalette.background2)
.size(62.dp)
) {
Image(
painter = painterResource(R.drawable.shuffle),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.align(Alignment.Center)
.size(20.dp)
)
}
} ?: viewModel.result?.exceptionOrNull()?.let { _ ->
Box(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures {
viewModel.fetch(browseId)
}
}
.align(Alignment.Center)
.fillMaxSize()
) {
BasicText(
text = "An error has occurred.\nTap to retry",
style = typography.s.medium.secondary.center,
modifier = Modifier
.align(Alignment.Center)
)
}
} ?: Column(
modifier = Modifier
.padding(LocalPlayerAwarePaddingValues.current)
.shimmer()
) {
HeaderPlaceholder()
Spacer(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(all = 16.dp)
.clip(thumbnailShape)
.size(thumbnailSizeDp)
.background(colorPalette.shimmer)
)
repeat(3) { index ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.alpha(1f - index * 0.25f)
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
.height(Dimensions.thumbnails.song)
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(Dimensions.thumbnails.song)
)
Column {
TextPlaceholder()
TextPlaceholder()
}
}
}
}
}
}

View file

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

View file

@ -10,6 +10,8 @@ import androidx.compose.ui.unit.dp
object Dimensions {
val itemsVerticalPadding = 8.dp
val verticalBarWidth = 64.dp
object thumbnails {
val album = 128.dp
val artist = 192.dp

View file

@ -11,6 +11,7 @@ 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.text.BasicText
@ -158,6 +159,7 @@ fun SongItem(
.fillMaxWidth()
.padding(vertical = Dimensions.itemsVerticalPadding)
.padding(start = 16.dp, end = if (trailingContent == null) 16.dp else 8.dp)
.height(Dimensions.thumbnails.song)
) {
startContent()