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,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
hoverOnly: Boolean = false,
|
hoverOnly: Boolean = false,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = colors ?: rememberButtonColors()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
@ -306,9 +307,10 @@ fun PrimaryButton(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = colors ?: rememberButtonColors()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
@ -365,9 +367,10 @@ fun SecondaryButton(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = colors ?: rememberButtonColors()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
@ -426,6 +429,7 @@ fun IconButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
@ -434,10 +438,12 @@ fun IconButton(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
colors = colors,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
||||||
fun GhostIconButton(
|
fun GhostIconButton(
|
||||||
@ -445,6 +451,7 @@ fun GhostIconButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
@ -454,6 +461,7 @@ fun GhostIconButton(
|
|||||||
shape = shape,
|
shape = shape,
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
hoverOnly = true,
|
hoverOnly = true,
|
||||||
|
colors = colors,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -465,6 +473,7 @@ fun PrimaryIconButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
@ -473,6 +482,7 @@ fun PrimaryIconButton(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
colors = colors,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -484,6 +494,7 @@ fun SecondaryIconButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
@ -492,6 +503,7 @@ fun SecondaryIconButton(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
colors = colors,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -501,8 +513,9 @@ fun SecondaryIconButton(
|
|||||||
fun ButtonBadge(
|
fun ButtonBadge(
|
||||||
count: Int,
|
count: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = colors ?: rememberButtonColors()
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.heightIn(min = 22.dp)
|
.heightIn(min = 22.dp)
|
||||||
@ -529,9 +542,10 @@ fun GroupButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 11.dp),
|
contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 11.dp),
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = colors ?: rememberButtonColors()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
@ -570,9 +584,10 @@ fun GroupIconButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = colors ?: rememberButtonColors()
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
@ -604,9 +619,10 @@ fun GroupIconButton(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ButtonGroup(
|
fun ButtonGroup(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
colors: ButtonColors? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = colors ?: rememberButtonColors()
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.defaultMinSize(minHeight = ButtonMinHeight)
|
.defaultMinSize(minHeight = ButtonMinHeight)
|
||||||
@ -634,8 +650,10 @@ fun ButtonGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ButtonGroupDivider() {
|
fun ButtonGroupDivider(
|
||||||
val colors = rememberButtonColors()
|
colors: ButtonColors? = null,
|
||||||
|
) {
|
||||||
|
val colors = colors ?: rememberButtonColors()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(1.dp)
|
.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.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@ -68,12 +66,16 @@ 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.draw.clip
|
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.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
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.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
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.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.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.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowSquareUp
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowSquareUp
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
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.IconsaxMusicFilter
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPrevious
|
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.IconsaxRepeatMusic
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeateMusic
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeateMusic
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeateOne
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxRepeateOne
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxShuffle
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxShuffle
|
||||||
|
import dev.krtirtho.spotube.resources.iconsax.InconsaxClock
|
||||||
import dev.krtirtho.spotube.resources.iconsax.SwapHorizontal2
|
import dev.krtirtho.spotube.resources.iconsax.SwapHorizontal2
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
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
|
// The expanded player on small screens
|
||||||
@ -278,15 +279,18 @@ fun AppExpandedPlayer(
|
|||||||
snackbarHostState.showSnackbar("Already downloaded")
|
snackbarHostState.showSnackbar("Already downloaded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadStatus.Failed, is DownloadStatus.Cancelled -> {
|
is DownloadStatus.Failed, is DownloadStatus.Cancelled -> {
|
||||||
download?.let { downloadsViewModel.retry(it.id) }
|
download?.let { downloadsViewModel.retry(it.id) }
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar("Retrying download...")
|
snackbarHostState.showSnackbar("Retrying download...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadStatus.Downloading, is DownloadStatus.Queued -> {
|
is DownloadStatus.Downloading, is DownloadStatus.Queued -> {
|
||||||
// already in progress
|
// already in progress
|
||||||
}
|
}
|
||||||
|
|
||||||
null -> {
|
null -> {
|
||||||
val track = currentTrack
|
val track = currentTrack
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
@ -393,7 +397,7 @@ fun AppExpandedPlayer(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onCollapse) {
|
GhostIconButton(onClick = onCollapse) {
|
||||||
Icon(Iconsax.IconsaxArrowDown4, contentDescription = "Collapse player")
|
Icon(Iconsax.IconsaxArrowDown4, contentDescription = "Collapse player")
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
@ -403,7 +407,7 @@ fun AppExpandedPlayer(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
IconButton(onClick = { showMoreOptionsSheet = true }) {
|
GhostIconButton(onClick = { showMoreOptionsSheet = true }) {
|
||||||
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options")
|
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -462,7 +466,7 @@ fun AppExpandedPlayer(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onQueue) {
|
IconButton(onClick = onQueue, shape = CircleShape) {
|
||||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -506,7 +510,7 @@ fun AppExpandedPlayer(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = ::onShuffleToggle) {
|
GhostIconButton(onClick = ::onShuffleToggle) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.IconsaxShuffle,
|
Iconsax.IconsaxShuffle,
|
||||||
contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle",
|
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")
|
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,
|
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(
|
Icon(
|
||||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier.size(30.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.surface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
GhostIconButton(onClick = ::onSkipNext) {
|
||||||
IconButton(onClick = ::onSkipNext) {
|
|
||||||
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
||||||
}
|
}
|
||||||
IconButton(onClick = ::onLoopToggle) {
|
GhostIconButton(onClick = ::onLoopToggle) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (playerUiState.loopState) {
|
imageVector = when (playerUiState.loopState) {
|
||||||
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
||||||
@ -639,14 +646,13 @@ private fun LyricsPreviewCard(
|
|||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
IconButton(
|
GhostIconButton(
|
||||||
onClick = onExpand,
|
onClick = onExpand,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.IconsaxArrowSquareUp,
|
Iconsax.IconsaxArrowSquareUp,
|
||||||
contentDescription = "Expand lyrics",
|
contentDescription = "Expand lyrics",
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
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.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.Spacer
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.LoopState
|
import dev.krtirtho.spotube.core.audioplayer.LoopState
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
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.DownloadProgressIcon
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||||
@ -93,7 +97,6 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
fun AppLargePlayer(
|
fun AppLargePlayer(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onQueue: () -> Unit = {},
|
onQueue: () -> Unit = {},
|
||||||
onDownload: () -> Unit = {},
|
|
||||||
onAlternativeSource: () -> Unit = {},
|
onAlternativeSource: () -> Unit = {},
|
||||||
onMoreOptions: () -> Unit = {},
|
onMoreOptions: () -> Unit = {},
|
||||||
onLyrics: () -> Unit = {},
|
onLyrics: () -> Unit = {},
|
||||||
@ -167,8 +170,8 @@ fun AppLargePlayer(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.78f),
|
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.78f),
|
||||||
tonalElevation = 4.dp,
|
tonalElevation = 0.dp,
|
||||||
shadowElevation = 14.dp
|
shadowElevation = 0.dp
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -209,6 +212,7 @@ fun AppLargePlayer(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
PlayerHeartButton(
|
PlayerHeartButton(
|
||||||
audioPlayerQueue = audioPlayerQueue,
|
audioPlayerQueue = audioPlayerQueue,
|
||||||
savedTracksViewModel = savedTracksViewModel,
|
savedTracksViewModel = savedTracksViewModel,
|
||||||
@ -253,7 +257,10 @@ fun AppLargePlayer(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = ::onShuffleToggle) {
|
VariableIconButton(
|
||||||
|
onClick = ::onShuffleToggle,
|
||||||
|
variant = if (playerUiState.isShuffling) VariableIconButtonVariant.Outline else VariableIconButtonVariant.Ghost
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Iconsax.IconsaxShuffle,
|
Iconsax.IconsaxShuffle,
|
||||||
contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle",
|
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")
|
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
||||||
}
|
}
|
||||||
IconButton(onClick = ::onPlayPause, modifier = Modifier.size(44.dp)) {
|
GhostIconButton(onClick = ::onPlayPause, modifier = Modifier.size(44.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause",
|
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = ::onSkipNext) {
|
GhostIconButton(onClick = ::onSkipNext) {
|
||||||
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
||||||
}
|
}
|
||||||
IconButton(onClick = ::onLoopToggle) {
|
VariableIconButton(
|
||||||
|
onClick = ::onLoopToggle,
|
||||||
|
variant = if (playerUiState.loopState == LoopState.NONE) VariableIconButtonVariant.Ghost else VariableIconButtonVariant.Outline
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (playerUiState.loopState) {
|
imageVector = when (playerUiState.loopState) {
|
||||||
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
||||||
@ -299,10 +309,10 @@ fun AppLargePlayer(
|
|||||||
horizontalAlignment = Alignment.End
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
IconButton(onClick = onQueue) {
|
GhostIconButton(onClick = onQueue) {
|
||||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
GhostIconButton(onClick = {
|
||||||
val track = (currentEntry as? QueueEntry.StreamingTrack)?.track
|
val track = (currentEntry as? QueueEntry.StreamingTrack)?.track
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
downloadsViewModel.downloadTrack(track)
|
downloadsViewModel.downloadTrack(track)
|
||||||
@ -314,18 +324,18 @@ fun AppLargePlayer(
|
|||||||
contentDescription = "Download",
|
contentDescription = "Download",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onAlternativeSource) {
|
GhostIconButton(onClick = onAlternativeSource) {
|
||||||
Icon(Iconsax.SwapHorizontal2, contentDescription = "Alternative source")
|
Icon(Iconsax.SwapHorizontal2, contentDescription = "Alternative source")
|
||||||
}
|
}
|
||||||
IconButton(onClick = onLyrics) {
|
GhostIconButton(onClick = onLyrics) {
|
||||||
Icon(Iconsax.IconsaxMusic, contentDescription = "Lyrics")
|
Icon(Iconsax.IconsaxMusic, contentDescription = "Lyrics")
|
||||||
}
|
}
|
||||||
IconButton(onClick = onMoreOptions) {
|
GhostIconButton(onClick = onMoreOptions) {
|
||||||
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "More options")
|
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "More options")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
IconButton(
|
GhostIconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (playerUiState.volume <= 0f) {
|
if (playerUiState.volume <= 0f) {
|
||||||
|
|||||||
@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
package dev.krtirtho.spotube.modules.shell
|
package dev.krtirtho.spotube.modules.shell
|
||||||
|
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -26,6 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
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.SavedState
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
|
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
|
||||||
@ -67,7 +68,8 @@ fun PlayerHeartButton(
|
|||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = ::onLike,
|
onClick = ::onLike,
|
||||||
enabled = isSavedTrackState is SavedState.Success
|
enabled = isSavedTrackState is SavedState.Success,
|
||||||
|
shape = CircleShape,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart,
|
imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user