Sync with master

This commit is contained in:
vfsfitvnm 2022-07-16 16:31:42 +02:00
parent 1e719b33ed
commit 5319c4094b
12 changed files with 1492 additions and 457 deletions

View file

@ -11,12 +11,12 @@ import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteDatabase
import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.enums.*
import it.vfsfitvnm.vimusic.models.*
import it.vfsfitvnm.vimusic.utils.getFloatOrNull
import it.vfsfitvnm.vimusic.utils.getLongOrNull
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@Dao
@ -31,6 +31,14 @@ interface Database {
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC")
fun songsByRowIdDesc(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title ASC")
fun songsByTitleAsc(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title DESC")
fun songsByTitleDesc(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY totalPlayTimeMs ASC")
fun songsByPlayTimeAsc(): Flow<List<DetailedSong>>
@ -45,6 +53,10 @@ interface Database {
SortOrder.Ascending -> songsByPlayTimeAsc()
SortOrder.Descending -> songsByPlayTimeDesc()
}
SongSortBy.Title -> when (sortOrder) {
SortOrder.Ascending -> songsByTitleAsc()
SortOrder.Descending -> songsByTitleDesc()
}
SongSortBy.DateAdded -> when (sortOrder) {
SortOrder.Ascending -> songsByRowIdAsc()
SortOrder.Descending -> songsByRowIdDesc()
@ -71,9 +83,72 @@ interface Database {
@Query("SELECT * FROM Artist WHERE id = :id")
fun artist(id: String): Flow<Artist?>
@Query("SELECT * FROM Artist ORDER BY name DESC")
fun artistsByNameDesc(): Flow<List<Artist>>
@Query("SELECT * FROM Artist ORDER BY name ASC")
fun artistsByNameAsc(): Flow<List<Artist>>
@Query("SELECT * FROM Artist ORDER BY ROWID DESC")
fun artistsByRowIdDesc(): Flow<List<Artist>>
@Query("SELECT * FROM Artist 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")
fun album(id: String): Flow<Album?>
@Query("SELECT * FROM Album ORDER BY ROWID ASC")
fun albums(): Flow<List<Album>>
@Query("SELECT * FROM Album ORDER BY title ASC")
fun albumsByTitleAsc(): Flow<List<Album>>
@Query("SELECT * FROM Album ORDER BY year ASC")
fun albumsByYearAsc(): Flow<List<Album>>
@Query("SELECT * FROM Album ORDER BY ROWID ASC")
fun albumsByRowIdAsc(): Flow<List<Album>>
@Query("SELECT * FROM Album ORDER BY title DESC")
fun albumsByTitleDesc(): Flow<List<Album>>
@Query("SELECT * FROM Album ORDER BY year DESC")
fun albumsByYearDesc(): Flow<List<Album>>
@Query("SELECT * FROM Album 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")
fun incrementTotalPlayTimeMs(id: String, addition: Long)
@ -82,8 +157,29 @@ interface Database {
fun playlistWithSongs(id: Long): Flow<PlaylistWithSongs?>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist")
fun playlistPreviews(): Flow<List<PlaylistPreview>>
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name ASC")
fun playlistPreviewsByName(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID ASC")
fun playlistPreviewsByDateAdded(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount ASC")
fun playlistPreviewsByDateSongCount(): Flow<List<PlaylistPreview>>
fun playlistPreviews(sortBy: PlaylistSortBy, sortOrder: SortOrder): Flow<List<PlaylistPreview>> {
return when (sortBy) {
PlaylistSortBy.Name -> playlistPreviewsByName()
PlaylistSortBy.DateAdded -> playlistPreviewsByDateAdded()
PlaylistSortBy.SongCount -> playlistPreviewsByDateSongCount()
}.map {
when (sortOrder) {
SortOrder.Ascending -> it
SortOrder.Descending -> it.reversed()
}
}
}
@Query("SELECT thumbnailUrl FROM Song JOIN SongPlaylistMap ON id = songId WHERE playlistId = :id ORDER BY position LIMIT 4")
fun playlistThumbnailUrls(id: Long): Flow<List<String?>>

View file

@ -0,0 +1,7 @@
package it.vfsfitvnm.vimusic.enums
enum class AlbumSortBy {
Title,
Year,
DateAdded
}

View file

@ -0,0 +1,6 @@
package it.vfsfitvnm.vimusic.enums
enum class ArtistSortBy {
Name,
DateAdded
}

View file

@ -0,0 +1,7 @@
package it.vfsfitvnm.vimusic.enums
enum class PlaylistSortBy {
Name,
DateAdded,
SongCount
}

View file

@ -2,5 +2,6 @@ package it.vfsfitvnm.vimusic.enums
enum class SongSortBy {
PlayTime,
Title,
DateAdded
}

View file

@ -23,10 +23,11 @@ import kotlin.math.absoluteValue
class TabPagerState(
val pageCount: Int,
val initialPageIndex: Int,
val onPageChanged: ((Int) -> Unit)?
) {
var pageIndex by mutableStateOf(initialPageIndex)
var tempPageIndex: Int? = null
var tempPageIndex by mutableStateOf<Int?>(null)
val animatable = Animatable(0f)
@ -57,15 +58,17 @@ class TabPagerState(
pageIndex = newPageIndex
animatable.snapTo(0f)
tempPageIndex = null
onPageChanged?.invoke(newPageIndex)
}
}
@Composable
fun rememberTabPagerState(initialPageIndex: Int, pageCount: Int): TabPagerState {
fun rememberTabPagerState(initialPageIndex: Int, pageCount: Int, onPageChanged: ((Int) -> Unit)? = null): TabPagerState {
return remember {
TabPagerState(
pageCount = pageCount,
initialPageIndex = initialPageIndex,
onPageChanged = onPageChanged
)
}
}
@ -122,6 +125,7 @@ fun HorizontalTabPager(
.plus(1)
.coerceAtMost(state.pageCount - 1)
state.animatable.snapTo(0f)
state.onPageChanged?.invoke(state.pageIndex)
}
} else {
state.animatable.animateTo(
@ -133,6 +137,7 @@ fun HorizontalTabPager(
.minus(1)
.coerceAtLeast(0)
state.animatable.snapTo(0f)
state.onPageChanged?.invoke(state.pageIndex)
}
}
}
@ -142,7 +147,7 @@ fun HorizontalTabPager(
) { constraints ->
val previousPlaceable = state.offset.takeIf { it < 0 }?.let {
(state.tempPageIndex ?: (state.pageIndex - 1)).takeIf { it >= 0 }?.let { index ->
measure(index, constraints).first()
measure(index, constraints).firstOrNull()
}
}
val placeable = measure(state.pageIndex, constraints).first()
@ -150,7 +155,7 @@ fun HorizontalTabPager(
val nextPlaceable = state.offset.takeIf { it > 0 }?.let {
(state.tempPageIndex ?: (state.pageIndex + 1)).takeIf { it < state.pageCount }
?.let { index ->
measure(index, constraints).first()
measure(index, constraints).firstOrNull()
}
}

View file

@ -0,0 +1,86 @@
package it.vfsfitvnm.vimusic.ui.components
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
data class TabPosition(
val left: Int,
val width: Int
)
@Composable
fun TabRow(
tabPagerState: TabPagerState,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val (colorPalette) = LocalAppearance.current
var tabPositions by remember {
mutableStateOf<List<TabPosition>?>(null)
}
val indicatorWidth by animateIntAsState(
targetValue = (tabPositions?.getOrNull(tabPagerState.transitioningIndex)?.width ?: 0)
)
val indicatorStart by animateIntAsState(
targetValue = (tabPositions?.getOrNull(tabPagerState.transitioningIndex)?.left ?: 0)
)
Layout(
modifier = modifier
.drawBehind {
if (indicatorWidth == 0) return@drawBehind
drawLine(
color = colorPalette.primaryContainer,
start = Offset(x = indicatorStart + 16.dp.toPx(), y = size.height),
end = Offset(
x = indicatorStart + indicatorWidth - 16.dp.toPx(),
y = size.height
),
cap = StrokeCap.Round,
strokeWidth = 3.dp.toPx()
)
},
content = content
) { measurables, constraints ->
val placeables = measurables.map {
it.measure(constraints)
}
if (tabPositions == null) {
var x = 0
tabPositions = placeables.map { placeable ->
TabPosition(
left = x,
width = placeable.width
).also {
x += placeable.width
}
}
}
layout(constraints.maxWidth, placeables.maxOf(Placeable::height)) {
var x = 0
placeables.fastForEach { placeable ->
placeable.place(x = x, y = constraints.minHeight / 2)
x += placeable.width
}
}
}
}
inline val TabPagerState.transitioningIndex: Int
get() = tempPageIndex ?: pageIndex

View file

@ -22,6 +22,8 @@ import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.route.empty
import it.vfsfitvnm.vimusic.*
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
@ -276,7 +278,7 @@ fun MediaItemMenu(
onGlobalRouteEmitted: (() -> Unit)? = null,
) {
val playlistPreviews by remember {
Database.playlistPreviews()
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending)
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
val viewPlaylistsRoute = rememberCreatePlaylistRoute()

View file

@ -71,7 +71,7 @@ fun BuiltInPlaylistScreen(
when (builtInPlaylist) {
BuiltInPlaylist.Favorites -> Database.favorites()
BuiltInPlaylist.Cached -> Database.songsByRowIdDesc().map { songs ->
songs.filter { song ->
songs.reversed().filter { song ->
song.song.contentLength?.let { contentLength ->
binder?.cache?.isCached(song.song.id, 0, contentLength)
} ?: false

View file

@ -1,10 +1,9 @@
package it.vfsfitvnm.vimusic.ui.views
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -12,23 +11,27 @@ 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.Brush
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.LocalDensity
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.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlin.math.roundToInt
@Composable
@ -36,66 +39,131 @@ fun PlaylistPreviewItem(
playlistPreview: PlaylistPreview,
modifier: Modifier = Modifier,
thumbnailSize: Dp = Dimensions.thumbnails.song,
trailingContent: (@Composable () -> Unit)? = null
) {
val (colorPalette, typography) = LocalAppearance.current
val density = LocalDensity.current
val thumbnailSizePx = density.run {
thumbnailSize.toPx().toInt()
thumbnailSize.toPx().roundToInt()
}
val thumbnails by remember(playlistPreview.playlist.id) {
Database.playlistThumbnailUrls(playlistPreview.playlist.id).distinctUntilChanged()
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
Box(
modifier = modifier
.background(colorPalette.lightBackground)
.size(thumbnailSize * 2)
) {
if (thumbnails.toSet().size == 1) {
AsyncImage(
model = thumbnails.first().thumbnail(thumbnailSizePx * 2),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(thumbnailSize * 2)
)
} else {
listOf(
Alignment.TopStart,
Alignment.TopEnd,
Alignment.BottomStart,
Alignment.BottomEnd
).forEachIndexed { index, alignment ->
PlaylistItem(
name = playlistPreview.playlist.name,
modifier = modifier,
thumbnailSize = thumbnailSize,
trailingContent = trailingContent,
songCount = playlistPreview.songCount,
imageContent = {
if (thumbnails.toSet().size == 1) {
AsyncImage(
model = thumbnails.getOrNull(index).thumbnail(thumbnailSizePx),
model = thumbnails.first().thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.align(alignment)
.size(thumbnailSize)
)
} else {
Box(
modifier = Modifier
.size(thumbnailSize)
) {
listOf(
Alignment.TopStart,
Alignment.TopEnd,
Alignment.BottomStart,
Alignment.BottomEnd
).forEachIndexed { index, alignment ->
AsyncImage(
model = thumbnails.getOrNull(index).thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.align(alignment)
.size(thumbnailSize / 2)
)
}
}
}
}
)
}
@Composable
fun BuiltInPlaylistItem(
@DrawableRes icon: Int,
colorTint: Color,
name: String,
modifier: Modifier = Modifier,
thumbnailSize: Dp = Dimensions.thumbnails.song
) {
PlaylistItem(
name = name,
modifier = modifier,
thumbnailSize = thumbnailSize,
songCount = null,
imageContent = {
Image(
painter = painterResource(icon),
contentDescription = null,
colorFilter = ColorFilter.tint(colorTint),
modifier = Modifier
.align(Alignment.Center)
.size(18.dp)
)
}
)
}
@Composable
fun PlaylistItem(
name: String,
songCount: Int?,
modifier: Modifier = Modifier,
thumbnailSize: Dp = Dimensions.thumbnails.song,
trailingContent: (@Composable () -> Unit)? = null,
imageContent: @Composable BoxScope.() -> Unit
) {
val (colorPalette, typography) = LocalAppearance.current
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.fillMaxWidth()
.padding(vertical = 5.dp)
.padding(start = 16.dp, end = if (trailingContent == null) 16.dp else 8.dp)
) {
Box(
modifier = Modifier
.clip(ThumbnailRoundness.shape)
.background(colorPalette.lightBackground)
.size(thumbnailSize),
content = imageContent
)
BasicText(
text = playlistPreview.playlist.name,
style = typography.xxs.semiBold.color(Color.White),
text = name,
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.75f)
)
)
)
.padding(horizontal = 8.dp, vertical = 4.dp)
.weight(1f)
)
songCount?.let {
BasicText(
text = "$songCount song${if (songCount == 1) "" else "s"}",
style = typography.xxs.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
trailingContent?.invoke()
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.core.content.edit
const val colorPaletteModeKey = "colorPaletteMode"
const val homeScreenPageIndexKey = "homeScreenPageIndex"
const val thumbnailRoundnessKey = "thumbnailRoundness"
const val isCachedPlaylistShownKey = "isCachedPlaylistShown"
const val coilDiskCacheMaxSizeKey = "coilDiskCacheMaxSize"
@ -52,6 +53,16 @@ fun rememberPreference(key: String, defaultValue: Boolean): MutableState<Boolean
}
}
@Composable
fun rememberPreference(key: String, defaultValue: Int): MutableState<Int> {
val context = LocalContext.current
return remember {
mutableStatePreferenceOf(context.preferences.getInt(key, defaultValue)) {
context.preferences.edit { putInt(key, it) }
}
}
}
@Composable
fun rememberPreference(key: String, defaultValue: String): MutableState<String> {
val context = LocalContext.current