mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
feat: replace IconButton with GhostIconButton in AppExpandedPlayer and AppLargePlayer for improved visual consistency
This commit is contained in:
parent
cb27514b9b
commit
4302a27afa
@ -238,9 +238,10 @@ fun OutlineButton(
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||
hoverOnly: Boolean = false,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
@ -306,9 +307,10 @@ fun PrimaryButton(
|
||||
enabled: Boolean = true,
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
@ -365,9 +367,10 @@ fun SecondaryButton(
|
||||
enabled: Boolean = true,
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
@ -426,6 +429,7 @@ fun IconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
OutlineButton(
|
||||
@ -434,10 +438,12 @@ fun IconButton(
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
colors = colors,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
fun GhostIconButton(
|
||||
@ -445,6 +451,7 @@ fun GhostIconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
OutlineButton(
|
||||
@ -454,6 +461,7 @@ fun GhostIconButton(
|
||||
shape = shape,
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
hoverOnly = true,
|
||||
colors = colors,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@ -465,6 +473,7 @@ fun PrimaryIconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
PrimaryButton(
|
||||
@ -473,6 +482,7 @@ fun PrimaryIconButton(
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
colors = colors,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@ -484,6 +494,7 @@ fun SecondaryIconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
SecondaryButton(
|
||||
@ -492,6 +503,7 @@ fun SecondaryIconButton(
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
colors = colors,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@ -501,8 +513,9 @@ fun SecondaryIconButton(
|
||||
fun ButtonBadge(
|
||||
count: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
colors: ButtonColors? = null,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
Box(
|
||||
modifier = modifier
|
||||
.heightIn(min = 22.dp)
|
||||
@ -529,9 +542,10 @@ fun GroupButton(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 11.dp),
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
@ -570,9 +584,10 @@ fun GroupIconButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
@ -604,9 +619,10 @@ fun GroupIconButton(
|
||||
@Composable
|
||||
fun ButtonGroup(
|
||||
modifier: Modifier = Modifier,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = ButtonMinHeight)
|
||||
@ -634,8 +650,10 @@ fun ButtonGroup(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ButtonGroupDivider() {
|
||||
val colors = rememberButtonColors()
|
||||
fun ButtonGroupDivider(
|
||||
colors: ButtonColors? = null,
|
||||
) {
|
||||
val colors = colors ?: rememberButtonColors()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(1.dp)
|
||||
@ -871,3 +889,59 @@ private fun ButtonStylesPreview() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class VariableIconButtonVariant {
|
||||
Outline,
|
||||
Ghost,
|
||||
Primary,
|
||||
Secondary,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VariableIconButton(
|
||||
variant: VariableIconButtonVariant = VariableIconButtonVariant.Outline,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||
colors: ButtonColors? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
when (variant) {
|
||||
VariableIconButtonVariant.Outline -> IconButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
content = content,
|
||||
)
|
||||
|
||||
VariableIconButtonVariant.Ghost -> GhostIconButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
content = content,
|
||||
)
|
||||
|
||||
VariableIconButtonVariant.Primary -> PrimaryIconButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
content = content,
|
||||
)
|
||||
|
||||
VariableIconButtonVariant.Secondary -> SecondaryIconButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package dev.krtirtho.spotube.core.ui.base
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val SliderTrackHeight = 7.dp
|
||||
private val SliderThumbSize = 24.dp
|
||||
private val SliderMinHeight = 32.dp
|
||||
|
||||
@Composable
|
||||
fun Slider(
|
||||
value: Float,
|
||||
onValueChange: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||
steps: Int = 0,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
onValueChangeFinished: () -> Unit = {},
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val density = LocalDensity.current
|
||||
var sliderWidth by remember { mutableIntStateOf(0) }
|
||||
|
||||
val range = valueRange.endInclusive - valueRange.start
|
||||
val fraction = if (range > 0f) ((value - valueRange.start) / range).coerceIn(0f, 1f) else 0f
|
||||
|
||||
val trackActiveColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f)
|
||||
val trackInactiveColor = if (enabled) {
|
||||
colors.border.copy(alpha = 0.6f)
|
||||
} else {
|
||||
colors.onContainer.copy(alpha = 0.2f)
|
||||
}
|
||||
val thumbColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f)
|
||||
|
||||
val thumbScale by animateFloatAsState(
|
||||
targetValue = when {
|
||||
isPressed -> 1.15f
|
||||
isHovered -> 1.08f
|
||||
else -> 1f
|
||||
},
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "thumbScale",
|
||||
)
|
||||
|
||||
val thumbElevation = when {
|
||||
isPressed -> 2.dp
|
||||
isHovered -> 8.dp
|
||||
else -> 5.dp
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = SliderMinHeight)
|
||||
.onSizeChanged { sliderWidth = it.width }
|
||||
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
||||
.pointerInput(enabled, valueRange, steps) {
|
||||
if (!enabled) return@pointerInput
|
||||
|
||||
val stepSize = if (steps > 0) range / (steps + 1) else 0f
|
||||
val thumbHalfPx = with(density) { SliderThumbSize.toPx() / 2f }
|
||||
|
||||
fun valueFromX(x: Float): Float {
|
||||
val width = size.width.toFloat()
|
||||
val trackWidth = width - 2 * thumbHalfPx
|
||||
if (trackWidth <= 0f) return valueRange.start
|
||||
val raw = ((x - thumbHalfPx) / trackWidth) * range + valueRange.start
|
||||
val coerced = raw.coerceIn(valueRange.start, valueRange.endInclusive)
|
||||
return if (steps > 0) {
|
||||
val stepIndex = ((coerced - valueRange.start) / stepSize).roundToInt()
|
||||
(valueRange.start + stepIndex * stepSize).coerceIn(
|
||||
valueRange.start,
|
||||
valueRange.endInclusive,
|
||||
)
|
||||
} else {
|
||||
coerced
|
||||
}
|
||||
}
|
||||
|
||||
detectTapGestures { offset ->
|
||||
onValueChange(valueFromX(offset.x))
|
||||
onValueChangeFinished()
|
||||
}
|
||||
}
|
||||
.pointerInput(enabled, valueRange, steps) {
|
||||
if (!enabled) return@pointerInput
|
||||
|
||||
val stepSize = if (steps > 0) range / (steps + 1) else 0f
|
||||
val thumbHalfPx = with(density) { SliderThumbSize.toPx() / 2f }
|
||||
|
||||
fun valueFromX(x: Float): Float {
|
||||
val width = size.width.toFloat()
|
||||
val trackWidth = width - 2 * thumbHalfPx
|
||||
if (trackWidth <= 0f) return valueRange.start
|
||||
val raw = ((x - thumbHalfPx) / trackWidth) * range + valueRange.start
|
||||
val coerced = raw.coerceIn(valueRange.start, valueRange.endInclusive)
|
||||
return if (steps > 0) {
|
||||
val stepIndex = ((coerced - valueRange.start) / stepSize).roundToInt()
|
||||
(valueRange.start + stepIndex * stepSize).coerceIn(
|
||||
valueRange.start,
|
||||
valueRange.endInclusive,
|
||||
)
|
||||
} else {
|
||||
coerced
|
||||
}
|
||||
}
|
||||
|
||||
detectHorizontalDragGestures(
|
||||
onDragEnd = { onValueChangeFinished() },
|
||||
) { change, _ ->
|
||||
change.consume()
|
||||
onValueChange(valueFromX(change.position.x))
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(SliderTrackHeight)
|
||||
.align(Alignment.CenterStart),
|
||||
) {
|
||||
val thumbHalfPx = SliderThumbSize.toPx() / 2f
|
||||
val trackY = size.height / 2f
|
||||
val trackStart = thumbHalfPx
|
||||
val trackEnd = size.width - thumbHalfPx
|
||||
val trackWidth = trackEnd - trackStart
|
||||
|
||||
if (trackWidth > 0f) {
|
||||
val trackRadius = CornerRadius(size.height / 2f)
|
||||
|
||||
drawRoundRect(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
trackInactiveColor.copy(alpha = 0.45f),
|
||||
trackInactiveColor,
|
||||
trackInactiveColor.copy(alpha = 0.7f),
|
||||
),
|
||||
),
|
||||
topLeft = Offset(trackStart, 0f),
|
||||
size = Size(trackWidth, size.height),
|
||||
cornerRadius = trackRadius,
|
||||
)
|
||||
drawLine(
|
||||
color = Color.White.copy(alpha = 0.15f),
|
||||
start = Offset(trackStart + size.height / 2f, 1.dp.toPx()),
|
||||
end = Offset(trackEnd - size.height / 2f, 1.dp.toPx()),
|
||||
strokeWidth = 1.dp.toPx(),
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
|
||||
val activeEnd = trackStart + trackWidth * fraction
|
||||
drawRoundRect(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
trackActiveColor.copy(alpha = 0.75f),
|
||||
trackActiveColor,
|
||||
trackActiveColor.copy(alpha = 0.9f),
|
||||
),
|
||||
),
|
||||
topLeft = Offset(trackStart, 0f),
|
||||
size = Size((activeEnd - trackStart).coerceAtLeast(0f), size.height),
|
||||
cornerRadius = trackRadius,
|
||||
)
|
||||
drawLine(
|
||||
color = Color.White.copy(alpha = 0.22f),
|
||||
start = Offset(trackStart + size.height / 2f, 1.dp.toPx()),
|
||||
end = Offset(activeEnd - size.height / 2f, 1.dp.toPx()),
|
||||
strokeWidth = 1.dp.toPx(),
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
|
||||
if (steps > 0 && enabled) {
|
||||
val stepSpacing = trackWidth / (steps + 1)
|
||||
for (i in 1 downTo steps) {
|
||||
val x = trackStart + stepSpacing * i
|
||||
drawCircle(
|
||||
color = if (x <= activeEnd) {
|
||||
colors.onAccent.copy(alpha = 0.5f)
|
||||
} else {
|
||||
colors.onContainer.copy(alpha = 0.25f)
|
||||
},
|
||||
radius = 2.dp.toPx(),
|
||||
center = Offset(x, trackY),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(SliderThumbSize)
|
||||
.offset {
|
||||
val thumbHalfPx = with(density) { SliderThumbSize.toPx() / 2f }
|
||||
val trackWidth = sliderWidth.toFloat() - 2 * thumbHalfPx
|
||||
val x = thumbHalfPx + trackWidth * fraction - thumbHalfPx
|
||||
IntOffset(x.roundToInt(), 0)
|
||||
}
|
||||
.graphicsLayer {
|
||||
scaleX = thumbScale
|
||||
scaleY = thumbScale
|
||||
}
|
||||
.shadow(
|
||||
elevation = thumbElevation,
|
||||
shape = CircleShape,
|
||||
ambientColor = colors.accent.copy(alpha = 0.35f),
|
||||
spotColor = colors.accent.copy(alpha = 0.5f),
|
||||
)
|
||||
.background(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.6f),
|
||||
thumbColor.copy(alpha = 0.95f),
|
||||
thumbColor,
|
||||
),
|
||||
center = Offset(0f, -0.55f),
|
||||
radius = 1.1f,
|
||||
),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.border(
|
||||
width = 0.8.dp,
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.55f),
|
||||
Color.White.copy(alpha = 0.08f),
|
||||
),
|
||||
),
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -45,11 +45,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
@ -68,12 +66,16 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil3.compose.AsyncImage
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
@ -81,42 +83,41 @@ import dev.krtirtho.spotube.core.audioplayer.LoopState
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.Slider
|
||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadStatus
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||
import dev.krtirtho.spotube.modules.lyrics.LyricsViewModel
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowSquareUp
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCloseSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicFilter
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPrevious
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRefreshRight
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeatMusic
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeateMusic
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeateOne
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxShuffle
|
||||
import dev.krtirtho.spotube.resources.iconsax.InconsaxClock
|
||||
import dev.krtirtho.spotube.resources.iconsax.SwapHorizontal2
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedState
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadStatus
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCloseSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRefreshRight
|
||||
import dev.krtirtho.spotube.resources.iconsax.InconsaxClock
|
||||
|
||||
|
||||
// The expanded player on small screens
|
||||
@ -278,15 +279,18 @@ fun AppExpandedPlayer(
|
||||
snackbarHostState.showSnackbar("Already downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
is DownloadStatus.Failed, is DownloadStatus.Cancelled -> {
|
||||
download?.let { downloadsViewModel.retry(it.id) }
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar("Retrying download...")
|
||||
}
|
||||
}
|
||||
|
||||
is DownloadStatus.Downloading, is DownloadStatus.Queued -> {
|
||||
// already in progress
|
||||
}
|
||||
|
||||
null -> {
|
||||
val track = currentTrack
|
||||
if (track != null) {
|
||||
@ -393,7 +397,7 @@ fun AppExpandedPlayer(
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onCollapse) {
|
||||
GhostIconButton(onClick = onCollapse) {
|
||||
Icon(Iconsax.IconsaxArrowDown4, contentDescription = "Collapse player")
|
||||
}
|
||||
Text(
|
||||
@ -403,7 +407,7 @@ fun AppExpandedPlayer(
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
IconButton(onClick = { showMoreOptionsSheet = true }) {
|
||||
GhostIconButton(onClick = { showMoreOptionsSheet = true }) {
|
||||
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options")
|
||||
}
|
||||
}
|
||||
@ -462,7 +466,7 @@ fun AppExpandedPlayer(
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onQueue) {
|
||||
IconButton(onClick = onQueue, shape = CircleShape) {
|
||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||
}
|
||||
}
|
||||
@ -506,7 +510,7 @@ fun AppExpandedPlayer(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = ::onShuffleToggle) {
|
||||
GhostIconButton(onClick = ::onShuffleToggle) {
|
||||
Icon(
|
||||
Iconsax.IconsaxShuffle,
|
||||
contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle",
|
||||
@ -517,28 +521,31 @@ fun AppExpandedPlayer(
|
||||
}
|
||||
)
|
||||
}
|
||||
IconButton(onClick = ::onSkipPrevious) {
|
||||
GhostIconButton(onClick = ::onSkipPrevious) {
|
||||
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
||||
}
|
||||
Surface(
|
||||
IconButton(
|
||||
onClick = ::onPlayPause,
|
||||
colors = rememberButtonColors().copy(
|
||||
containerDarker = MaterialTheme.colorScheme.onSurface,
|
||||
containerLighter = MaterialTheme.colorScheme.onSurface,
|
||||
containerPressed = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(72.dp),
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
contentColor = MaterialTheme.colorScheme.surface,
|
||||
shadowElevation = 8.dp,
|
||||
modifier = Modifier.size(72.dp),
|
||||
) {
|
||||
IconButton(onClick = ::onPlayPause, modifier = Modifier.fillMaxSize()) {
|
||||
Icon(
|
||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
||||
modifier = Modifier.size(30.dp),
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = ::onSkipNext) {
|
||||
GhostIconButton(onClick = ::onSkipNext) {
|
||||
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
||||
}
|
||||
IconButton(onClick = ::onLoopToggle) {
|
||||
GhostIconButton(onClick = ::onLoopToggle) {
|
||||
Icon(
|
||||
imageVector = when (playerUiState.loopState) {
|
||||
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
||||
@ -639,14 +646,13 @@ private fun LyricsPreviewCard(
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
IconButton(
|
||||
GhostIconButton(
|
||||
onClick = onExpand,
|
||||
modifier = Modifier.size(24.dp),
|
||||
modifier = Modifier.size(28.dp),
|
||||
) {
|
||||
Icon(
|
||||
Iconsax.IconsaxArrowSquareUp,
|
||||
contentDescription = "Expand lyrics",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@ -22,15 +22,15 @@ 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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -53,6 +53,10 @@ import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.LoopState
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.Slider
|
||||
import dev.krtirtho.spotube.core.ui.base.VariableIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.VariableIconButtonVariant
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||
@ -93,7 +97,6 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
fun AppLargePlayer(
|
||||
modifier: Modifier = Modifier,
|
||||
onQueue: () -> Unit = {},
|
||||
onDownload: () -> Unit = {},
|
||||
onAlternativeSource: () -> Unit = {},
|
||||
onMoreOptions: () -> Unit = {},
|
||||
onLyrics: () -> Unit = {},
|
||||
@ -167,8 +170,8 @@ fun AppLargePlayer(
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.78f),
|
||||
tonalElevation = 4.dp,
|
||||
shadowElevation = 14.dp
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 0.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -209,6 +212,7 @@ fun AppLargePlayer(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
PlayerHeartButton(
|
||||
audioPlayerQueue = audioPlayerQueue,
|
||||
savedTracksViewModel = savedTracksViewModel,
|
||||
@ -253,7 +257,10 @@ fun AppLargePlayer(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
IconButton(onClick = ::onShuffleToggle) {
|
||||
VariableIconButton(
|
||||
onClick = ::onShuffleToggle,
|
||||
variant = if (playerUiState.isShuffling) VariableIconButtonVariant.Outline else VariableIconButtonVariant.Ghost
|
||||
) {
|
||||
Icon(
|
||||
Iconsax.IconsaxShuffle,
|
||||
contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle",
|
||||
@ -264,19 +271,22 @@ fun AppLargePlayer(
|
||||
}
|
||||
)
|
||||
}
|
||||
IconButton(onClick = ::onSkipPrevious) {
|
||||
GhostIconButton(onClick = ::onSkipPrevious) {
|
||||
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
||||
}
|
||||
IconButton(onClick = ::onPlayPause, modifier = Modifier.size(44.dp)) {
|
||||
GhostIconButton(onClick = ::onPlayPause, modifier = Modifier.size(44.dp)) {
|
||||
Icon(
|
||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause",
|
||||
)
|
||||
}
|
||||
IconButton(onClick = ::onSkipNext) {
|
||||
GhostIconButton(onClick = ::onSkipNext) {
|
||||
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
||||
}
|
||||
IconButton(onClick = ::onLoopToggle) {
|
||||
VariableIconButton(
|
||||
onClick = ::onLoopToggle,
|
||||
variant = if (playerUiState.loopState == LoopState.NONE) VariableIconButtonVariant.Ghost else VariableIconButtonVariant.Outline
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (playerUiState.loopState) {
|
||||
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
||||
@ -299,10 +309,10 @@ fun AppLargePlayer(
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onQueue) {
|
||||
GhostIconButton(onClick = onQueue) {
|
||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||
}
|
||||
IconButton(onClick = {
|
||||
GhostIconButton(onClick = {
|
||||
val track = (currentEntry as? QueueEntry.StreamingTrack)?.track
|
||||
if (track != null) {
|
||||
downloadsViewModel.downloadTrack(track)
|
||||
@ -314,18 +324,18 @@ fun AppLargePlayer(
|
||||
contentDescription = "Download",
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onAlternativeSource) {
|
||||
GhostIconButton(onClick = onAlternativeSource) {
|
||||
Icon(Iconsax.SwapHorizontal2, contentDescription = "Alternative source")
|
||||
}
|
||||
IconButton(onClick = onLyrics) {
|
||||
GhostIconButton(onClick = onLyrics) {
|
||||
Icon(Iconsax.IconsaxMusic, contentDescription = "Lyrics")
|
||||
}
|
||||
IconButton(onClick = onMoreOptions) {
|
||||
GhostIconButton(onClick = onMoreOptions) {
|
||||
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "More options")
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(
|
||||
GhostIconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (playerUiState.volume <= 0f) {
|
||||
|
||||
@ -17,8 +17,8 @@
|
||||
|
||||
package dev.krtirtho.spotube.modules.shell
|
||||
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -26,6 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedState
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
|
||||
@ -67,7 +68,8 @@ fun PlayerHeartButton(
|
||||
}
|
||||
IconButton(
|
||||
onClick = ::onLike,
|
||||
enabled = isSavedTrackState is SavedState.Success
|
||||
enabled = isSavedTrackState is SavedState.Success,
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user