mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
ui nits: cover placeholder ratio, now playing scrim/title, add-server keyboard, titlebars, search dismiss, fab icon
- CoverArt: use the thumbnail's intrinsic ratio until the full-res painter loads, so non-square art no longer snaps - Now Playing: move the top scrim out of the sliding screen into MainNavigation, static under the status bar, fading in as the screen settles at the top - Now Playing: titlebar shows the playback source (album/playlist name, or Library) instead of "Now playing" - Add server: keep the test/save bar above the keyboard on first launch - Titlebars: use the theme default title style instead of headlineMedium - Library: clicking an item in a searched tab dismisses the search - Library FAB: use the AutoMirrored Sort icon at 28dp so the glyph reads a little thicker
This commit is contained in:
@@ -13,6 +13,7 @@ import com.subtracks.data.model.ShuffleOrder
|
|||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
import com.subtracks.data.prefs.SongSort
|
import com.subtracks.data.prefs.SongSort
|
||||||
import com.subtracks.data.prefs.StarredFilter
|
import com.subtracks.data.prefs.StarredFilter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
@@ -119,6 +120,19 @@ class QueueRepository(
|
|||||||
return if (index >= 0) index.toLong() else null
|
return if (index >= 0) index.toLong() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun sourceName(
|
||||||
|
kind: QueueKind,
|
||||||
|
sourceId: Long,
|
||||||
|
refId: String,
|
||||||
|
): String? {
|
||||||
|
val library = db.libraryDao()
|
||||||
|
return when (kind) {
|
||||||
|
QueueKind.Album -> library.album(sourceId, refId).first()?.name
|
||||||
|
QueueKind.Playlist -> library.playlist(sourceId, refId).first()?.name
|
||||||
|
QueueKind.Songs, QueueKind.Song -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun songsQuery(refId: String): QueueSongQuery? {
|
private fun songsQuery(refId: String): QueueSongQuery? {
|
||||||
val parts = refId.split("|")
|
val parts = refId.split("|")
|
||||||
if (parts.size != 3) return null
|
if (parts.size != 3) return null
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ data class QueueItem(
|
|||||||
|
|
||||||
data class QueueContext(
|
data class QueueContext(
|
||||||
val kind: QueueKind,
|
val kind: QueueKind,
|
||||||
|
val sourceId: Long,
|
||||||
val refId: String,
|
val refId: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -889,9 +890,11 @@ class PlaybackController(
|
|||||||
|
|
||||||
private fun contextAt(position: Long?): QueueContext? {
|
private fun contextAt(position: Long?): QueueContext? {
|
||||||
val entry = position?.let { snapshot?.locate(it)?.first } ?: return null
|
val entry = position?.let { snapshot?.locate(it)?.first } ?: return null
|
||||||
return QueueContext(entry.kind, entry.refId)
|
return QueueContext(entry.kind, entry.sourceId, entry.refId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun sourceTitle(context: QueueContext?): String? = context?.let { queueRepository.sourceName(it.kind, it.sourceId, it.refId) }
|
||||||
|
|
||||||
private fun updateBuffering() {
|
private fun updateBuffering() {
|
||||||
if (player?.isBuffering != true) {
|
if (player?.isBuffering != true) {
|
||||||
bufferingJob?.cancel()
|
bufferingJob?.cancel()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.compose.animation.slideInHorizontally
|
|||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.draggable
|
import androidx.compose.foundation.gestures.draggable
|
||||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
@@ -23,9 +24,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imeAnimationTarget
|
import androidx.compose.foundation.layout.imeAnimationTarget
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
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
|
||||||
@@ -37,6 +41,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
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
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
@@ -88,6 +94,7 @@ private const val OVERLAY_DURATION_MS = 200
|
|||||||
private const val EXPAND_FADE = 0.1f
|
private const val EXPAND_FADE = 0.1f
|
||||||
private const val FLING_VELOCITY = 1000f
|
private const val FLING_VELOCITY = 1000f
|
||||||
private const val MINI_PLAYER_ANIM_MS = 200
|
private const val MINI_PLAYER_ANIM_MS = 200
|
||||||
|
private const val SCRIM_FADE_START = 0.6f
|
||||||
|
|
||||||
private object Routes {
|
private object Routes {
|
||||||
const val LIBRARY = "library"
|
const val LIBRARY = "library"
|
||||||
@@ -163,6 +170,7 @@ private fun MainNavigation() {
|
|||||||
}
|
}
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||||
|
val statusBarInset = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||||
val keyboardUp = WindowInsets.imeAnimationTarget.getBottom(density) > 0
|
val keyboardUp = WindowInsets.imeAnimationTarget.getBottom(density) > 0
|
||||||
val showMiniPlayer = playerVisible && !keyboardUp
|
val showMiniPlayer = playerVisible && !keyboardUp
|
||||||
val bottomInset by animateDpAsState(
|
val bottomInset by animateDpAsState(
|
||||||
@@ -333,6 +341,21 @@ private fun MainNavigation() {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(statusBarInset + 8.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = ((nowPlayingProgress - SCRIM_FADE_START) / (1f - SCRIM_FADE_START)).coerceIn(0f, 1f)
|
||||||
|
}.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(Color.Black.copy(alpha = 0.55f), Color.Transparent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|||||||
@@ -85,8 +85,16 @@ fun CoverArt(
|
|||||||
onError = { failed = true },
|
onError = { failed = true },
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
|
val thumbnailPainter =
|
||||||
|
rememberAsyncImagePainter(
|
||||||
|
model = remember(thumbnailRef) { thumbnailRef?.let { imageRequest(context, it, crossfade = false) } },
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
BoxWithConstraints(modifier, contentAlignment = Alignment.Center) {
|
BoxWithConstraints(modifier, contentAlignment = Alignment.Center) {
|
||||||
val ratio = painter.intrinsicSize.ratioOrOne()
|
val ratio =
|
||||||
|
painter.intrinsicSize.ratioOrNull()
|
||||||
|
?: thumbnailPainter.intrinsicSize.ratioOrNull()
|
||||||
|
?: 1f
|
||||||
val width = if (ratio > maxWidth.value / maxHeight.value) maxWidth else maxHeight * ratio
|
val width = if (ratio > maxWidth.value / maxHeight.value) maxWidth else maxHeight * ratio
|
||||||
Box(Modifier.size(width, width / ratio).then(frameModifier)) {
|
Box(Modifier.size(width, width / ratio).then(frameModifier)) {
|
||||||
CoverArtContent(
|
CoverArtContent(
|
||||||
@@ -146,7 +154,7 @@ private fun BoxScope.CoverArtContent(
|
|||||||
main()
|
main()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Size.ratioOrOne(): Float = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else 1f
|
private fun Size.ratioOrNull(): Float? = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else null
|
||||||
|
|
||||||
private fun imageRequest(
|
private fun imageRequest(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ fun HeroDetailScaffold(
|
|||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.graphicsLayer { alpha = barFraction },
|
modifier = Modifier.graphicsLayer { alpha = barFraction },
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import androidx.compose.foundation.pager.rememberPagerState
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||||
import androidx.compose.material.icons.automirrored.rounded.PlaylistPlay
|
import androidx.compose.material.icons.automirrored.rounded.PlaylistPlay
|
||||||
import androidx.compose.material.icons.filled.Sort
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Album
|
import androidx.compose.material.icons.rounded.Album
|
||||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
import androidx.compose.material.icons.rounded.ArrowDownward
|
||||||
@@ -304,6 +304,13 @@ fun LibraryScreen(
|
|||||||
if (search.isNotEmpty()) searchActive = true
|
if (search.isNotEmpty()) searchActive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismissSearch() {
|
||||||
|
if (searchActive) {
|
||||||
|
searchActive = false
|
||||||
|
onSearchChange("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||||
val filtersActive = listQuery.starred != StarredFilter.Any || search.isNotEmpty()
|
val filtersActive = listQuery.starred != StarredFilter.Any || search.isNotEmpty()
|
||||||
@@ -347,7 +354,10 @@ fun LibraryScreen(
|
|||||||
albums,
|
albums,
|
||||||
coverArt,
|
coverArt,
|
||||||
listBottomInset,
|
listBottomInset,
|
||||||
onAlbumClick,
|
{
|
||||||
|
dismissSearch()
|
||||||
|
onAlbumClick(it)
|
||||||
|
},
|
||||||
filtered = filtersActive,
|
filtered = filtersActive,
|
||||||
onClearFilters = onClearFilters,
|
onClearFilters = onClearFilters,
|
||||||
resetKey = resetKey,
|
resetKey = resetKey,
|
||||||
@@ -361,7 +371,10 @@ fun LibraryScreen(
|
|||||||
artists,
|
artists,
|
||||||
coverArt,
|
coverArt,
|
||||||
listBottomInset,
|
listBottomInset,
|
||||||
onArtistClick,
|
{
|
||||||
|
dismissSearch()
|
||||||
|
onArtistClick(it)
|
||||||
|
},
|
||||||
filtered = filtersActive,
|
filtered = filtersActive,
|
||||||
onClearFilters = onClearFilters,
|
onClearFilters = onClearFilters,
|
||||||
resetKey = resetKey,
|
resetKey = resetKey,
|
||||||
@@ -375,7 +388,10 @@ fun LibraryScreen(
|
|||||||
songs,
|
songs,
|
||||||
coverArt,
|
coverArt,
|
||||||
listBottomInset,
|
listBottomInset,
|
||||||
onSongClick,
|
{
|
||||||
|
dismissSearch()
|
||||||
|
onSongClick(it)
|
||||||
|
},
|
||||||
playingSongId,
|
playingSongId,
|
||||||
filtered = filtersActive,
|
filtered = filtersActive,
|
||||||
onClearFilters = onClearFilters,
|
onClearFilters = onClearFilters,
|
||||||
@@ -390,7 +406,10 @@ fun LibraryScreen(
|
|||||||
playlists,
|
playlists,
|
||||||
coverArt,
|
coverArt,
|
||||||
listBottomInset,
|
listBottomInset,
|
||||||
onPlaylistClick,
|
{
|
||||||
|
dismissSearch()
|
||||||
|
onPlaylistClick(it)
|
||||||
|
},
|
||||||
filtered = filtersActive,
|
filtered = filtersActive,
|
||||||
onClearFilters = onClearFilters,
|
onClearFilters = onClearFilters,
|
||||||
resetKey = resetKey,
|
resetKey = resetKey,
|
||||||
@@ -460,7 +479,11 @@ fun LibraryScreen(
|
|||||||
modifier = Modifier.align(Alignment.BottomEnd).padding(end = 16.dp, bottom = tabBarHeight + 16.dp),
|
modifier = Modifier.align(Alignment.BottomEnd).padding(end = 16.dp, bottom = tabBarHeight + 16.dp),
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
Icon(Icons.Filled.Sort, contentDescription = "List options")
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.Sort,
|
||||||
|
contentDescription = "List options",
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
)
|
||||||
if (filtersActive) {
|
if (filtersActive) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
|
|||||||
@@ -7,13 +7,11 @@ 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
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
@@ -45,11 +43,11 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.PlatformTextStyle
|
import androidx.compose.ui.text.PlatformTextStyle
|
||||||
@@ -61,6 +59,7 @@ import coil3.SingletonImageLoader
|
|||||||
import coil3.compose.LocalPlatformContext
|
import coil3.compose.LocalPlatformContext
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
|
import com.subtracks.data.model.QueueKind
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.playback.PlaybackState
|
import com.subtracks.playback.PlaybackState
|
||||||
import com.subtracks.playback.RepeatMode
|
import com.subtracks.playback.RepeatMode
|
||||||
@@ -83,6 +82,11 @@ fun NowPlayingRoute(
|
|||||||
val context = LocalPlatformContext.current
|
val context = LocalPlatformContext.current
|
||||||
val art = controller.coverArt(state.item)
|
val art = controller.coverArt(state.item)
|
||||||
val thumbnail = controller.coverArt(state.item, thumbnail = true)
|
val thumbnail = controller.coverArt(state.item, thumbnail = true)
|
||||||
|
val queueContext = state.context
|
||||||
|
val fallbackTitle = if (queueContext?.kind == QueueKind.Album) state.item?.album.orEmpty() else "Library"
|
||||||
|
val sourceTitle by produceState<String>(initialValue = fallbackTitle, queueContext) {
|
||||||
|
value = controller.sourceTitle(queueContext) ?: fallbackTitle
|
||||||
|
}
|
||||||
LaunchedEffect(state.item?.id, state.hasNext) {
|
LaunchedEffect(state.item?.id, state.hasNext) {
|
||||||
if (!state.hasNext) return@LaunchedEffect
|
if (!state.hasNext) return@LaunchedEffect
|
||||||
controller.upcomingCoverArt(thumbnail = true)?.let { ArtworkSeedCache.prefetch(context, it) }
|
controller.upcomingCoverArt(thumbnail = true)?.let { ArtworkSeedCache.prefetch(context, it) }
|
||||||
@@ -90,6 +94,7 @@ fun NowPlayingRoute(
|
|||||||
}
|
}
|
||||||
NowPlayingScreen(
|
NowPlayingScreen(
|
||||||
state = state,
|
state = state,
|
||||||
|
title = sourceTitle,
|
||||||
coverArt = art,
|
coverArt = art,
|
||||||
thumbnailRef = thumbnail,
|
thumbnailRef = thumbnail,
|
||||||
artwork = rememberArtworkColors(thumbnail ?: art),
|
artwork = rememberArtworkColors(thumbnail ?: art),
|
||||||
@@ -123,6 +128,7 @@ private fun prefetchImage(
|
|||||||
@Composable
|
@Composable
|
||||||
fun NowPlayingScreen(
|
fun NowPlayingScreen(
|
||||||
state: PlaybackState,
|
state: PlaybackState,
|
||||||
|
title: String,
|
||||||
coverArt: CoverArtRef?,
|
coverArt: CoverArtRef?,
|
||||||
artwork: ArtworkColors?,
|
artwork: ArtworkColors?,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
@@ -157,19 +163,6 @@ fun NowPlayingScreen(
|
|||||||
scrollPx = { 0f },
|
scrollPx = { 0f },
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
val statusBarDp = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.align(Alignment.TopStart)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(statusBarDp + 8.dp)
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
listOf(Color.Black.copy(alpha = 0.55f), Color.Transparent),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
@@ -177,8 +170,7 @@ fun NowPlayingScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = "Now playing",
|
text = title,
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ fun QueueScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Queue", style = MaterialTheme.typography.headlineMedium) },
|
title = { Text("Queue") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package com.subtracks.ui.settings
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -56,7 +58,7 @@ fun AddSourceRoute(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddSourceScreen(
|
fun AddSourceScreen(
|
||||||
state: AddSourceState,
|
state: AddSourceState,
|
||||||
@@ -74,7 +76,7 @@ fun AddSourceScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Add server", style = MaterialTheme.typography.headlineMedium) },
|
title = { Text("Add server") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
if (onBack != null) {
|
if (onBack != null) {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
@@ -86,7 +88,12 @@ fun AddSourceScreen(
|
|||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().navigationBarsPadding().padding(16.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ fun SettingsScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Settings", style = MaterialTheme.typography.headlineMedium) },
|
title = { Text("Settings") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class PlaybackControllerTest {
|
|||||||
await { handle.operations.contains("play") }
|
await { handle.operations.contains("play") }
|
||||||
|
|
||||||
assertEquals(51, handle.itemCount)
|
assertEquals(51, handle.itemCount)
|
||||||
assertEquals(QueueContext(QueueKind.Songs, ""), controller.state.value.context)
|
assertEquals(QueueContext(QueueKind.Songs, 1, ""), controller.state.value.context)
|
||||||
assertTrue(controller.state.value.hasNext)
|
assertTrue(controller.state.value.hasNext)
|
||||||
assertTrue(controller.state.value.hasPrevious)
|
assertTrue(controller.state.value.hasPrevious)
|
||||||
}
|
}
|
||||||
@@ -305,7 +305,7 @@ class PlaybackControllerTest {
|
|||||||
controller.playAlbum(1, "al1", 0)
|
controller.playAlbum(1, "al1", 0)
|
||||||
await { controller.state.value.item != null }
|
await { controller.state.value.item != null }
|
||||||
|
|
||||||
assertEquals(QueueContext(QueueKind.Album, "al1"), controller.state.value.context)
|
assertEquals(QueueContext(QueueKind.Album, 1, "al1"), controller.state.value.context)
|
||||||
assertTrue(controller.state.value.isPlaying)
|
assertTrue(controller.state.value.isPlaying)
|
||||||
assertTrue(controller.state.value.hasNext)
|
assertTrue(controller.state.value.hasNext)
|
||||||
assertTrue(controller.state.value.hasPrevious)
|
assertTrue(controller.state.value.hasPrevious)
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ class AppScreenshotTest {
|
|||||||
SubtracksTheme {
|
SubtracksTheme {
|
||||||
NowPlayingScreen(
|
NowPlayingScreen(
|
||||||
state = Fixtures.playbackState(),
|
state = Fixtures.playbackState(),
|
||||||
|
title = "Kid A",
|
||||||
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
|
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
|
||||||
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
|
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
@@ -392,6 +393,7 @@ class AppScreenshotTest {
|
|||||||
SubtracksTheme {
|
SubtracksTheme {
|
||||||
NowPlayingScreen(
|
NowPlayingScreen(
|
||||||
state = Fixtures.playbackState().copy(shuffle = true, repeat = RepeatMode.One),
|
state = Fixtures.playbackState().copy(shuffle = true, repeat = RepeatMode.One),
|
||||||
|
title = "Kid A",
|
||||||
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
|
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
|
||||||
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
|
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
|
||||||
onBack = {},
|
onBack = {},
|
||||||
|
|||||||
Reference in New Issue
Block a user