mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-06 04:09:51 +00:00
Compare commits
1 Commits
c4b5ae6600
...
088948f414
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
088948f414 |
@ -238,10 +238,9 @@ 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 = colors ?: rememberButtonColors()
|
val 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()
|
||||||
@ -307,10 +306,9 @@ 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 = colors ?: rememberButtonColors()
|
val 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()
|
||||||
@ -367,10 +365,9 @@ 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 = colors ?: rememberButtonColors()
|
val 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()
|
||||||
@ -429,7 +426,6 @@ 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(
|
||||||
@ -438,12 +434,10 @@ 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(
|
||||||
@ -451,7 +445,6 @@ 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(
|
||||||
@ -461,7 +454,6 @@ fun GhostIconButton(
|
|||||||
shape = shape,
|
shape = shape,
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
hoverOnly = true,
|
hoverOnly = true,
|
||||||
colors = colors,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -473,7 +465,6 @@ 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(
|
||||||
@ -482,7 +473,6 @@ fun PrimaryIconButton(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
colors = colors,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -494,7 +484,6 @@ 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(
|
||||||
@ -503,7 +492,6 @@ fun SecondaryIconButton(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
colors = colors,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -513,9 +501,8 @@ fun SecondaryIconButton(
|
|||||||
fun ButtonBadge(
|
fun ButtonBadge(
|
||||||
count: Int,
|
count: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
colors: ButtonColors? = null,
|
|
||||||
) {
|
) {
|
||||||
val colors = colors ?: rememberButtonColors()
|
val colors = rememberButtonColors()
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.heightIn(min = 22.dp)
|
.heightIn(min = 22.dp)
|
||||||
@ -542,10 +529,9 @@ 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 = colors ?: rememberButtonColors()
|
val 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()
|
||||||
@ -584,10 +570,9 @@ 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 = colors ?: rememberButtonColors()
|
val 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()
|
||||||
@ -619,10 +604,9 @@ 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 = colors ?: rememberButtonColors()
|
val colors = rememberButtonColors()
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.defaultMinSize(minHeight = ButtonMinHeight)
|
.defaultMinSize(minHeight = ButtonMinHeight)
|
||||||
@ -650,10 +634,8 @@ fun ButtonGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ButtonGroupDivider(
|
fun ButtonGroupDivider() {
|
||||||
colors: ButtonColors? = null,
|
val colors = rememberButtonColors()
|
||||||
) {
|
|
||||||
val colors = colors ?: rememberButtonColors()
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(1.dp)
|
.width(1.dp)
|
||||||
@ -889,59 +871,3 @@ 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,299 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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,9 +45,11 @@ 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
|
||||||
@ -66,16 +68,12 @@ 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
|
||||||
@ -83,41 +81,42 @@ 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.SAVED_TRACKS_COLLECTION_ID
|
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||||
|
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||||
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.IconsaxCd
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCheckCircle
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
|
||||||
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
|
||||||
@ -279,18 +278,15 @@ 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) {
|
||||||
@ -397,7 +393,7 @@ fun AppExpandedPlayer(
|
|||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
GhostIconButton(onClick = onCollapse) {
|
IconButton(onClick = onCollapse) {
|
||||||
Icon(Iconsax.IconsaxArrowDown4, contentDescription = "Collapse player")
|
Icon(Iconsax.IconsaxArrowDown4, contentDescription = "Collapse player")
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
@ -407,7 +403,7 @@ fun AppExpandedPlayer(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
GhostIconButton(onClick = { showMoreOptionsSheet = true }) {
|
IconButton(onClick = { showMoreOptionsSheet = true }) {
|
||||||
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options")
|
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,7 +462,7 @@ fun AppExpandedPlayer(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onQueue, shape = CircleShape) {
|
IconButton(onClick = onQueue) {
|
||||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -510,7 +506,7 @@ fun AppExpandedPlayer(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
GhostIconButton(onClick = ::onShuffleToggle) {
|
IconButton(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",
|
||||||
@ -521,31 +517,28 @@ fun AppExpandedPlayer(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onSkipPrevious) {
|
IconButton(onClick = ::onSkipPrevious) {
|
||||||
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
||||||
}
|
}
|
||||||
IconButton(
|
Surface(
|
||||||
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),
|
||||||
) {
|
) {
|
||||||
Icon(
|
IconButton(onClick = ::onPlayPause, modifier = Modifier.fillMaxSize()) {
|
||||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
Icon(
|
||||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||||
modifier = Modifier.size(30.dp),
|
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
||||||
tint = MaterialTheme.colorScheme.surface,
|
modifier = Modifier.size(30.dp),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onSkipNext) {
|
IconButton(onClick = ::onSkipNext) {
|
||||||
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onLoopToggle) {
|
IconButton(onClick = ::onLoopToggle) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = when (playerUiState.loopState) {
|
imageVector = when (playerUiState.loopState) {
|
||||||
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
LoopState.NONE -> Iconsax.IconsaxRepeateMusic
|
||||||
@ -646,13 +639,14 @@ private fun LyricsPreviewCard(
|
|||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
GhostIconButton(
|
IconButton(
|
||||||
onClick = onExpand,
|
onClick = onExpand,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(24.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,10 +53,6 @@ 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
|
||||||
@ -97,6 +93,7 @@ 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 = {},
|
||||||
@ -170,8 +167,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 = 0.dp,
|
tonalElevation = 4.dp,
|
||||||
shadowElevation = 0.dp
|
shadowElevation = 14.dp
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -212,7 +209,6 @@ fun AppLargePlayer(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
PlayerHeartButton(
|
PlayerHeartButton(
|
||||||
audioPlayerQueue = audioPlayerQueue,
|
audioPlayerQueue = audioPlayerQueue,
|
||||||
savedTracksViewModel = savedTracksViewModel,
|
savedTracksViewModel = savedTracksViewModel,
|
||||||
@ -257,10 +253,7 @@ fun AppLargePlayer(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
VariableIconButton(
|
IconButton(onClick = ::onShuffleToggle) {
|
||||||
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",
|
||||||
@ -271,22 +264,19 @@ fun AppLargePlayer(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onSkipPrevious) {
|
IconButton(onClick = ::onSkipPrevious) {
|
||||||
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onPlayPause, modifier = Modifier.size(44.dp)) {
|
IconButton(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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onSkipNext) {
|
IconButton(onClick = ::onSkipNext) {
|
||||||
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
Icon(Iconsax.IconsaxNext, contentDescription = "Next")
|
||||||
}
|
}
|
||||||
VariableIconButton(
|
IconButton(onClick = ::onLoopToggle) {
|
||||||
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
|
||||||
@ -309,10 +299,10 @@ fun AppLargePlayer(
|
|||||||
horizontalAlignment = Alignment.End
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
GhostIconButton(onClick = onQueue) {
|
IconButton(onClick = onQueue) {
|
||||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = {
|
IconButton(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)
|
||||||
@ -324,18 +314,18 @@ fun AppLargePlayer(
|
|||||||
contentDescription = "Download",
|
contentDescription = "Download",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = onAlternativeSource) {
|
IconButton(onClick = onAlternativeSource) {
|
||||||
Icon(Iconsax.SwapHorizontal2, contentDescription = "Alternative source")
|
Icon(Iconsax.SwapHorizontal2, contentDescription = "Alternative source")
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = onLyrics) {
|
IconButton(onClick = onLyrics) {
|
||||||
Icon(Iconsax.IconsaxMusic, contentDescription = "Lyrics")
|
Icon(Iconsax.IconsaxMusic, contentDescription = "Lyrics")
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = onMoreOptions) {
|
IconButton(onClick = onMoreOptions) {
|
||||||
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "More options")
|
Icon(Iconsax.Iconsax3DotsMore, contentDescription = "More options")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
GhostIconButton(
|
IconButton(
|
||||||
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,7 +26,6 @@ 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
|
||||||
@ -68,8 +67,7 @@ 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