mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-09 05:39:26 +00:00
Compare commits
1 Commits
ccfd28f4a5
...
36819165da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36819165da |
@ -19,7 +19,6 @@ package dev.krtirtho.spotube.core.audioplayer
|
|||||||
|
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||||
import dev.krtirtho.spotube.core.di.injectLogger
|
import dev.krtirtho.spotube.core.di.injectLogger
|
||||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
|
||||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -37,14 +36,12 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class DeviceAudioPlayerQueue(
|
class DeviceAudioPlayerQueue(
|
||||||
private val audioPlayer: AudioPlayer,
|
private val audioPlayer: AudioPlayer,
|
||||||
private val settingsViewModel: SettingsViewModel,
|
private val settingsViewModel: SettingsViewModel,
|
||||||
private val repository: AudioPlayerQueueRepository,
|
private val repository: AudioPlayerQueueRepository,
|
||||||
private val pluginManager: PluginManager,
|
|
||||||
) : AudioPlayerQueue, KoinComponent {
|
) : AudioPlayerQueue, KoinComponent {
|
||||||
|
|
||||||
private val logger by injectLogger<DeviceAudioPlayerQueue>()
|
private val logger by injectLogger<DeviceAudioPlayerQueue>()
|
||||||
@ -101,27 +98,6 @@ class DeviceAudioPlayerQueue(
|
|||||||
persistQueueStateIfReady()
|
persistQueueStateIfReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
|
||||||
combine(
|
|
||||||
audioPlayer.currentMediaItemFlow,
|
|
||||||
audioPlayer.playlistFlow,
|
|
||||||
audioPlayer.loopStateFlow,
|
|
||||||
) { currentMedia, playlist, loopState ->
|
|
||||||
Triple(currentMedia, playlist, loopState)
|
|
||||||
}.collect { (currentMedia, playlist, loopState) ->
|
|
||||||
if (currentMedia == null || playlist.isEmpty()) return@collect
|
|
||||||
if (loopState == LoopState.ALL) return@collect
|
|
||||||
|
|
||||||
val currentIndex = playlist.indexOfFirst { it.url == currentMedia.url }
|
|
||||||
if (currentIndex < 0) return@collect
|
|
||||||
|
|
||||||
val tracksAfterCurrent = playlist.size - currentIndex - 1
|
|
||||||
if (tracksAfterCurrent <= 0) {
|
|
||||||
handleQueueCompletion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(
|
override suspend fun load(
|
||||||
@ -305,61 +281,6 @@ class DeviceAudioPlayerQueue(
|
|||||||
return collectionHistoryState.value
|
return collectionHistoryState.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isFetchingRecommendations = false
|
|
||||||
|
|
||||||
private suspend fun handleQueueCompletion() {
|
|
||||||
if (isFetchingRecommendations) return
|
|
||||||
|
|
||||||
val settings = settingsViewModel.settingsState.value ?: return
|
|
||||||
if (!settings.enableEndlessPlayback) return
|
|
||||||
|
|
||||||
val queue = queueFlow.value
|
|
||||||
val streamingTracks = queue.filterIsInstance<QueueEntry.StreamingTrack>()
|
|
||||||
if (streamingTracks.isEmpty()) return
|
|
||||||
|
|
||||||
val seedTracks = if (streamingTracks.size <= 5) {
|
|
||||||
streamingTracks
|
|
||||||
} else {
|
|
||||||
val shuffled = streamingTracks.shuffled(Random)
|
|
||||||
shuffled.take(5)
|
|
||||||
}
|
|
||||||
|
|
||||||
val seedTrackIds = seedTracks.map { it.track.id }
|
|
||||||
if (seedTrackIds.isEmpty()) return
|
|
||||||
|
|
||||||
isFetchingRecommendations = true
|
|
||||||
logger.i { "Endless playback: fetching recommendations with ${seedTrackIds.size} seed tracks" }
|
|
||||||
|
|
||||||
val metadataService = pluginManager.selectedMetadataPlugin.value ?: run {
|
|
||||||
logger.w { "Endless playback: no metadata plugin available" }
|
|
||||||
isFetchingRecommendations = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val recommendations = metadataService.use {
|
|
||||||
metadataTrackAPI.recommendationsBasedOnTracks(seedTrackIds, limit = 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recommendations.isEmpty()) {
|
|
||||||
logger.w { "Endless playback: no recommendations returned" }
|
|
||||||
isFetchingRecommendations = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val newEntries = recommendations.map { track ->
|
|
||||||
QueueEntry.StreamingTrack(track = track, url = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.i { "Endless playback: adding ${newEntries.size} recommended tracks to queue" }
|
|
||||||
addAllToQueue(newEntries)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.e(e) { "Endless playback: failed to fetch recommendations" }
|
|
||||||
} finally {
|
|
||||||
isFetchingRecommendations = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun restorePersistedState() {
|
private suspend fun restorePersistedState() {
|
||||||
val state = repository.getPersistedState()
|
val state = repository.getPersistedState()
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
|
|||||||
@ -163,7 +163,7 @@ val sharedModules = module {
|
|||||||
}
|
}
|
||||||
singleOf(::AudioPlayerQueueRepository)
|
singleOf(::AudioPlayerQueueRepository)
|
||||||
single<AudioPlayerQueue> {
|
single<AudioPlayerQueue> {
|
||||||
DeviceAudioPlayerQueue(get(), get(), get(), get())
|
DeviceAudioPlayerQueue(get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
factory { (tag: String?) ->
|
factory { (tag: String?) ->
|
||||||
|
|||||||
@ -1,481 +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.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.hoverable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.focusable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
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.clip
|
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.input.key.Key
|
|
||||||
import androidx.compose.ui.input.key.KeyEvent
|
|
||||||
import androidx.compose.ui.input.key.key
|
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.IntSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Popup
|
|
||||||
import androidx.compose.ui.window.PopupProperties
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
|
||||||
|
|
||||||
private val AutocompleteTextFieldShape = RoundedCornerShape(14.dp)
|
|
||||||
private val AutocompleteMenuShape = RoundedCornerShape(12.dp)
|
|
||||||
private val AutocompleteTextFieldMinHeight = 44.dp
|
|
||||||
private val AutocompleteMenuDefaultMaxHeight = 300.dp
|
|
||||||
private val AutocompleteMenuDefaultOffset = 4.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun <T> AutocompleteTextField(
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
items: List<T>,
|
|
||||||
itemContent: @Composable (item: T, isSelected: Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onItemSelected: (T) -> Unit = {},
|
|
||||||
placeholder: @Composable (() -> Unit)? = null,
|
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
|
||||||
trailingIcon: @Composable (() -> Unit)? = null,
|
|
||||||
label: @Composable (() -> Unit)? = null,
|
|
||||||
singleLine: Boolean = true,
|
|
||||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
readOnly: Boolean = false,
|
|
||||||
isError: Boolean = false,
|
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
|
||||||
textStyle: TextStyle = TextStyle.Default,
|
|
||||||
cursorBrush: Color = MaterialTheme.colorScheme.primary,
|
|
||||||
onKeyEvent: ((KeyEvent) -> Boolean)? = null,
|
|
||||||
menuMaxHeight: Dp = AutocompleteMenuDefaultMaxHeight,
|
|
||||||
menuOffset: Dp = AutocompleteMenuDefaultOffset,
|
|
||||||
menuShape: Shape = AutocompleteMenuShape,
|
|
||||||
menuShadowElevation: Dp = 8.dp,
|
|
||||||
menuContainerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
) {
|
|
||||||
val density = LocalDensity.current
|
|
||||||
val colors = rememberButtonColors()
|
|
||||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
|
||||||
|
|
||||||
var isMenuOpen by remember { mutableStateOf(false) }
|
|
||||||
var isMovingFocusToPopup by remember { mutableStateOf(false) }
|
|
||||||
var selectedIndex by remember { mutableIntStateOf(-1) }
|
|
||||||
var textFieldSize by remember { mutableStateOf(IntSize.Zero) }
|
|
||||||
val menuListState = rememberLazyListState()
|
|
||||||
val popupFocusRequester = remember { FocusRequester() }
|
|
||||||
val textFieldFocusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
val menuExpanded = isMenuOpen && items.isNotEmpty()
|
|
||||||
|
|
||||||
LaunchedEffect(isFocused) {
|
|
||||||
if (isFocused) {
|
|
||||||
isMenuOpen = true
|
|
||||||
isMovingFocusToPopup = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(value) {
|
|
||||||
if (selectedIndex != -1) selectedIndex = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(selectedIndex) {
|
|
||||||
if (selectedIndex >= 0 && menuExpanded) {
|
|
||||||
menuListState.scrollToItem(selectedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val wrappedKeyboardActions = remember(keyboardActions, menuExpanded, selectedIndex, items) {
|
|
||||||
KeyboardActions(
|
|
||||||
onSearch = {
|
|
||||||
if (menuExpanded && selectedIndex in items.indices) {
|
|
||||||
onItemSelected(items[selectedIndex])
|
|
||||||
selectedIndex = -1
|
|
||||||
isMenuOpen = false
|
|
||||||
} else {
|
|
||||||
keyboardActions.onSearch?.invoke(this)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone = keyboardActions.onDone,
|
|
||||||
onGo = keyboardActions.onGo,
|
|
||||||
onNext = keyboardActions.onNext,
|
|
||||||
onPrevious = keyboardActions.onPrevious,
|
|
||||||
onSend = keyboardActions.onSend,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val gradient = outlinedGradient(colors, false)
|
|
||||||
val border = when {
|
|
||||||
isError -> MaterialTheme.colorScheme.error
|
|
||||||
isFocused -> MaterialTheme.colorScheme.primary
|
|
||||||
isHovered -> colors.border.copy(alpha = 0.85f)
|
|
||||||
else -> colors.border
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentColor = when {
|
|
||||||
!enabled -> colors.onContainer.copy(alpha = 0.38f)
|
|
||||||
else -> colors.onContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
fun navigate(delta: Int) {
|
|
||||||
if (items.isEmpty()) return
|
|
||||||
selectedIndex = ((selectedIndex + delta) % items.size + items.size) % items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
|
||||||
Column {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = label != null,
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut(),
|
|
||||||
) {
|
|
||||||
label?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.onSizeChanged { textFieldSize = it }
|
|
||||||
.defaultMinSize(minHeight = AutocompleteTextFieldMinHeight)
|
|
||||||
.then(textFieldShadow(AutocompleteTextFieldShape, isFocused, colors))
|
|
||||||
.clip(AutocompleteTextFieldShape)
|
|
||||||
.background(gradient, AutocompleteTextFieldShape)
|
|
||||||
.border(BorderStroke(.5.dp, border), AutocompleteTextFieldShape)
|
|
||||||
.drawWithCache {
|
|
||||||
val highlightBrush = Brush.verticalGradient(
|
|
||||||
colors = listOf(colors.highlight, Color.Transparent),
|
|
||||||
startY = 0f,
|
|
||||||
endY = size.height * 0.5f,
|
|
||||||
)
|
|
||||||
onDrawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
brush = highlightBrush,
|
|
||||||
topLeft = androidx.compose.ui.geometry.Offset.Zero,
|
|
||||||
size = size,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
if (leadingIcon != null) {
|
|
||||||
leadingIcon()
|
|
||||||
}
|
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
|
||||||
BasicTextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.hoverable(
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
enabled = enabled,
|
|
||||||
)
|
|
||||||
.focusRequester(textFieldFocusRequester)
|
|
||||||
.onFocusChanged { state ->
|
|
||||||
if (!state.isFocused && !isMovingFocusToPopup) {
|
|
||||||
isMenuOpen = false
|
|
||||||
selectedIndex = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onPreviewKeyEvent { event ->
|
|
||||||
if (onKeyEvent?.invoke(event) == true) {
|
|
||||||
return@onPreviewKeyEvent true
|
|
||||||
}
|
|
||||||
if (!menuExpanded) return@onPreviewKeyEvent false
|
|
||||||
when (event.key) {
|
|
||||||
Key.DirectionDown -> {
|
|
||||||
isMovingFocusToPopup = true
|
|
||||||
popupFocusRequester.requestFocus()
|
|
||||||
navigate(1)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.DirectionUp -> {
|
|
||||||
if (selectedIndex >= 0) {
|
|
||||||
navigate(-1)
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.Enter -> {
|
|
||||||
if (selectedIndex in items.indices) {
|
|
||||||
onItemSelected(items[selectedIndex])
|
|
||||||
selectedIndex = -1
|
|
||||||
isMenuOpen = false
|
|
||||||
return@onPreviewKeyEvent true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.Escape -> {
|
|
||||||
selectedIndex = -1
|
|
||||||
isMenuOpen = false
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = enabled,
|
|
||||||
readOnly = readOnly,
|
|
||||||
textStyle = textStyle.copy(color = contentColor),
|
|
||||||
cursorBrush = SolidColor(cursorBrush),
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = wrappedKeyboardActions,
|
|
||||||
singleLine = singleLine,
|
|
||||||
maxLines = maxLines,
|
|
||||||
visualTransformation = visualTransformation,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
if (value.isEmpty() && placeholder != null && !isFocused) {
|
|
||||||
CompositionLocalProvider(LocalTextStyle provides textStyle) {
|
|
||||||
Box(contentAlignment = Alignment.CenterStart) {
|
|
||||||
placeholder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trailingIcon != null) {
|
|
||||||
trailingIcon()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (menuExpanded && textFieldSize.width > 0) {
|
|
||||||
val textFieldHeightPx = textFieldSize.height
|
|
||||||
val menuOffsetPx = with(density) { menuOffset.roundToPx() }
|
|
||||||
val textFieldWidthDp = with(density) { textFieldSize.width.toDp() }
|
|
||||||
|
|
||||||
Popup(
|
|
||||||
alignment = Alignment.TopStart,
|
|
||||||
offset = IntOffset(0, textFieldHeightPx + menuOffsetPx),
|
|
||||||
properties = PopupProperties(focusable = true),
|
|
||||||
onDismissRequest = {
|
|
||||||
selectedIndex = -1
|
|
||||||
isMenuOpen = false
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(textFieldWidthDp)
|
|
||||||
.heightIn(max = menuMaxHeight)
|
|
||||||
.shadow(menuShadowElevation, menuShape)
|
|
||||||
.background(menuContainerColor, menuShape)
|
|
||||||
.clip(menuShape)
|
|
||||||
.focusRequester(popupFocusRequester)
|
|
||||||
.focusable()
|
|
||||||
.onFocusChanged { state ->
|
|
||||||
if (state.isFocused) {
|
|
||||||
isMovingFocusToPopup = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onKeyEvent { event ->
|
|
||||||
when (event.key) {
|
|
||||||
Key.DirectionDown -> {
|
|
||||||
navigate(1); true
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.DirectionUp -> {
|
|
||||||
navigate(-1); true
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.Enter -> {
|
|
||||||
if (selectedIndex in items.indices) {
|
|
||||||
onItemSelected(items[selectedIndex])
|
|
||||||
selectedIndex = -1
|
|
||||||
isMenuOpen = false
|
|
||||||
textFieldFocusRequester.requestFocus()
|
|
||||||
return@onKeyEvent true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
Key.Escape -> {
|
|
||||||
selectedIndex = -1
|
|
||||||
isMenuOpen = false
|
|
||||||
textFieldFocusRequester.requestFocus()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
state = menuListState,
|
|
||||||
modifier = Modifier
|
|
||||||
.heightIn(max = menuMaxHeight),
|
|
||||||
) {
|
|
||||||
items(items.size) { index ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
onItemSelected(items[index])
|
|
||||||
selectedIndex = -1
|
|
||||||
isMenuOpen = false
|
|
||||||
textFieldFocusRequester.requestFocus()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
itemContent(items[index], selectedIndex == index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun AutocompleteTextFieldPreview() {
|
|
||||||
MaterialTheme {
|
|
||||||
androidx.compose.material3.Surface(
|
|
||||||
color = MaterialTheme.colorScheme.background,
|
|
||||||
modifier = Modifier.padding(24.dp),
|
|
||||||
) {
|
|
||||||
val artists = remember {
|
|
||||||
listOf(
|
|
||||||
"Twenty One Pilots",
|
|
||||||
"The Beatles",
|
|
||||||
"Adele",
|
|
||||||
"Drake",
|
|
||||||
"Taylor Swift",
|
|
||||||
"Arctic Monkeys",
|
|
||||||
"Billie Eilish",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var value by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
AutocompleteTextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = {
|
|
||||||
value = it
|
|
||||||
},
|
|
||||||
items = artists.filter {
|
|
||||||
it.contains(value, ignoreCase = true) && value.isNotEmpty()
|
|
||||||
},
|
|
||||||
onItemSelected = { selected ->
|
|
||||||
value = selected
|
|
||||||
},
|
|
||||||
placeholder = { Text("Search artists...") },
|
|
||||||
leadingIcon = {
|
|
||||||
androidx.compose.material3.Icon(
|
|
||||||
imageVector = Iconsax.IconsaxSearchBroken,
|
|
||||||
contentDescription = "Search",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
itemContent = { item, isSelected ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
color = if (isSelected) {
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
|
|
||||||
} else {
|
|
||||||
Color.Transparent
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
androidx.compose.material3.Icon(
|
|
||||||
imageVector = Iconsax.IconsaxSearchBroken,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = item,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -255,7 +255,7 @@ fun OutlineButton(
|
|||||||
.then(buttonShadow(shape, isPressed, primary = false, colors, hovered = isHovered))
|
.then(buttonShadow(shape, isPressed, primary = false, colors, hovered = isHovered))
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(gradient, shape)
|
.background(gradient, shape)
|
||||||
.border(BorderStroke(0.5.dp, border), shape)
|
.border(BorderStroke(1.5.dp, border), shape)
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
@ -312,7 +312,7 @@ fun PrimaryButton(
|
|||||||
.then(buttonShadow(shape, isPressed, primary = true, colors, hovered = isHovered))
|
.then(buttonShadow(shape, isPressed, primary = true, colors, hovered = isHovered))
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(gradient, shape)
|
.background(gradient, shape)
|
||||||
.border(BorderStroke(0.5.dp, colors.accent), shape)
|
.border(BorderStroke(1.5.dp, colors.accent), shape)
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
@ -372,7 +372,7 @@ fun SecondaryButton(
|
|||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(gradient, shape)
|
.background(gradient, shape)
|
||||||
.border(
|
.border(
|
||||||
BorderStroke(0.5.dp, colors.secondaryContainer.copy(alpha = 0.5f)),
|
BorderStroke(1.5.dp, colors.secondaryContainer.copy(alpha = 0.5f)),
|
||||||
shape,
|
shape,
|
||||||
)
|
)
|
||||||
.clickable(
|
.clickable(
|
||||||
@ -582,7 +582,7 @@ fun ButtonGroup(
|
|||||||
.then(buttonShadow(GroupShape, pressed = false, primary = false, colors, hovered = false)),
|
.then(buttonShadow(GroupShape, pressed = false, primary = false, colors, hovered = false)),
|
||||||
shape = GroupShape,
|
shape = GroupShape,
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
border = BorderStroke(0.5.dp, colors.border),
|
border = BorderStroke(1.5.dp, colors.border),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.background(outlinedGradient(colors, false), GroupShape),
|
modifier = Modifier.background(outlinedGradient(colors, false), GroupShape),
|
||||||
@ -599,7 +599,7 @@ fun ButtonGroupDivider() {
|
|||||||
val colors = rememberButtonColors()
|
val colors = rememberButtonColors()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(1.dp)
|
.width(1.5.dp)
|
||||||
.heightIn(min = 20.dp)
|
.heightIn(min = 20.dp)
|
||||||
.background(colors.border),
|
.background(colors.border),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -96,7 +96,7 @@ fun CheckBox(
|
|||||||
isFilled -> colors.accent
|
isFilled -> colors.accent
|
||||||
else -> colors.border
|
else -> colors.border
|
||||||
}
|
}
|
||||||
val borderWidth = 0.5.dp
|
val borderWidth = if (isFilled) 1.5.dp else 1.5.dp
|
||||||
|
|
||||||
val shadowElevation = when {
|
val shadowElevation = when {
|
||||||
isPressed -> 1.dp
|
isPressed -> 1.dp
|
||||||
|
|||||||
@ -65,7 +65,7 @@ private val TextFieldShape = RoundedCornerShape(14.dp)
|
|||||||
private val TextFieldMinHeight = 44.dp
|
private val TextFieldMinHeight = 44.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun textFieldShadow(
|
private fun textFieldShadow(
|
||||||
shape: androidx.compose.ui.graphics.Shape,
|
shape: androidx.compose.ui.graphics.Shape,
|
||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
colors: ButtonColors,
|
colors: ButtonColors,
|
||||||
@ -142,7 +142,7 @@ fun TextField(
|
|||||||
.then(textFieldShadow(TextFieldShape, isFocused, colors))
|
.then(textFieldShadow(TextFieldShape, isFocused, colors))
|
||||||
.clip(TextFieldShape)
|
.clip(TextFieldShape)
|
||||||
.background(gradient, TextFieldShape)
|
.background(gradient, TextFieldShape)
|
||||||
.border(BorderStroke(0.5.dp, border), TextFieldShape)
|
.border(BorderStroke(1.5.dp, border), TextFieldShape)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(colors.highlight, Color.Transparent),
|
colors = listOf(colors.highlight, Color.Transparent),
|
||||||
|
|||||||
@ -272,8 +272,8 @@ private fun HomeSection(
|
|||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
if (!subtitle.isNullOrEmpty()) Text(
|
Text(
|
||||||
text = subtitle,
|
text = subtitle ?: "",
|
||||||
style = MaterialTheme.typography.labelMedium.copy(
|
style = MaterialTheme.typography.labelMedium.copy(
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
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.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
@ -49,6 +50,8 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -57,20 +60,30 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
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.graphics.Color
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
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.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import compose.icons.FeatherIcons
|
import compose.icons.FeatherIcons
|
||||||
import compose.icons.feathericons.X
|
import compose.icons.feathericons.X
|
||||||
@ -83,12 +96,12 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.user.MetadataUser
|
|||||||
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.share.ShareService
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.base.AutocompleteTextField
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.ChipTab
|
import dev.krtirtho.spotube.core.ui.base.ChipTab
|
||||||
import dev.krtirtho.spotube.core.ui.component.AlbumCard
|
import dev.krtirtho.spotube.core.ui.component.AlbumCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||||
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
import dev.krtirtho.spotube.core.ui.component.ArtistCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||||
import dev.krtirtho.spotube.core.ui.component.PlaylistCard
|
import dev.krtirtho.spotube.core.ui.component.PlaylistCard
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackList
|
import dev.krtirtho.spotube.core.ui.component.TrackList
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
@ -109,6 +122,8 @@ import org.koin.compose.koinInject
|
|||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
private val GridMinCellSize = 180.dp
|
private val GridMinCellSize = 180.dp
|
||||||
|
private val SearchFieldShape = RoundedCornerShape(6.dp)
|
||||||
|
private val TabShape = RoundedCornerShape(6.dp)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
||||||
@ -120,7 +135,10 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
var isSearchFocused by remember { mutableStateOf(false) }
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
var keyboardSelectedIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
|
||||||
fun playSingleTrack(track: MetadataTrack) {
|
fun playSingleTrack(track: MetadataTrack) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -224,6 +242,30 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showRecentSearches =
|
||||||
|
isSearchFocused && state.query.isBlank() && state.recentSearches.isNotEmpty()
|
||||||
|
val dropdownItems = if (showRecentSearches) state.recentSearches else emptyList()
|
||||||
|
val totalDropdownItems = if (showRecentSearches) dropdownItems.size + 1 else 0
|
||||||
|
|
||||||
|
fun navigateDropdown(delta: Int) {
|
||||||
|
if (totalDropdownItems == 0) return
|
||||||
|
keyboardSelectedIndex =
|
||||||
|
(keyboardSelectedIndex + delta + totalDropdownItems) % totalDropdownItems
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectDropdownItem() {
|
||||||
|
if (keyboardSelectedIndex < 0 || !showRecentSearches) return
|
||||||
|
if (keyboardSelectedIndex == 0) {
|
||||||
|
viewModel.clearAllRecentSearches()
|
||||||
|
} else {
|
||||||
|
val item = dropdownItems[keyboardSelectedIndex - 1]
|
||||||
|
viewModel.applyRecentSearch(item)
|
||||||
|
}
|
||||||
|
keyboardSelectedIndex = -1
|
||||||
|
isSearchFocused = false
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { ApplicationMainBar(backButton = false) }
|
topBar = { ApplicationMainBar(backButton = false) }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
@ -237,19 +279,60 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
) {
|
) {
|
||||||
SearchBar(
|
SearchBar(
|
||||||
query = state.query,
|
query = state.query,
|
||||||
onQueryChange = viewModel::onQueryChange,
|
onQueryChange = {
|
||||||
|
viewModel.onQueryChange(it)
|
||||||
|
keyboardSelectedIndex = -1
|
||||||
|
},
|
||||||
onClear = viewModel::clearQuery,
|
onClear = viewModel::clearQuery,
|
||||||
|
isFocused = isSearchFocused,
|
||||||
|
onFocusChanged = { isSearchFocused = it },
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
onKeyEvent = { event ->
|
||||||
|
when (event.key) {
|
||||||
|
Key.DirectionDown -> {
|
||||||
|
navigateDropdown(1); true
|
||||||
|
}
|
||||||
|
|
||||||
|
Key.DirectionUp -> {
|
||||||
|
navigateDropdown(-1); true
|
||||||
|
}
|
||||||
|
|
||||||
|
Key.Enter -> {
|
||||||
|
selectDropdownItem(); true
|
||||||
|
}
|
||||||
|
|
||||||
|
Key.Escape -> {
|
||||||
|
isSearchFocused = false
|
||||||
|
keyboardSelectedIndex = -1
|
||||||
|
focusManager.clearFocus()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
},
|
||||||
onSearch = {
|
onSearch = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
|
isSearchFocused = false
|
||||||
|
},
|
||||||
|
showDropdown = showRecentSearches,
|
||||||
|
onDismissDropdown = {
|
||||||
|
isSearchFocused = false
|
||||||
|
keyboardSelectedIndex = -1
|
||||||
},
|
},
|
||||||
recentSearches = state.recentSearches,
|
recentSearches = state.recentSearches,
|
||||||
|
keyboardSelectedIndex = keyboardSelectedIndex,
|
||||||
onRecentSearchClick = { search ->
|
onRecentSearchClick = { search ->
|
||||||
viewModel.applyRecentSearch(search)
|
viewModel.applyRecentSearch(search)
|
||||||
|
isSearchFocused = false
|
||||||
|
keyboardSelectedIndex = -1
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
},
|
},
|
||||||
onRecentSearchRemove = viewModel::removeRecentSearch,
|
onRecentSearchRemove = viewModel::removeRecentSearch,
|
||||||
onClearAllRecentSearches = {
|
onClearAllRecentSearches = {
|
||||||
viewModel.clearAllRecentSearches()
|
viewModel.clearAllRecentSearches()
|
||||||
|
isSearchFocused = false
|
||||||
|
keyboardSelectedIndex = -1
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -360,57 +443,35 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed interface SearchDropdownEntry {
|
|
||||||
val onClick: () -> Unit
|
|
||||||
|
|
||||||
data class ClearAll(override val onClick: () -> Unit) : SearchDropdownEntry
|
|
||||||
data class RecentSearch(
|
|
||||||
val query: String,
|
|
||||||
override val onClick: () -> Unit,
|
|
||||||
val onRemove: () -> Unit,
|
|
||||||
) : SearchDropdownEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchBar(
|
private fun SearchBar(
|
||||||
query: String,
|
query: String,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
onClear: () -> Unit,
|
onClear: () -> Unit,
|
||||||
|
isFocused: Boolean,
|
||||||
|
onFocusChanged: (Boolean) -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
onKeyEvent: (androidx.compose.ui.input.key.KeyEvent) -> Boolean,
|
||||||
onSearch: () -> Unit,
|
onSearch: () -> Unit,
|
||||||
|
showDropdown: Boolean,
|
||||||
|
onDismissDropdown: () -> Unit,
|
||||||
recentSearches: List<String>,
|
recentSearches: List<String>,
|
||||||
|
keyboardSelectedIndex: Int,
|
||||||
onRecentSearchClick: (String) -> Unit,
|
onRecentSearchClick: (String) -> Unit,
|
||||||
onRecentSearchRemove: (String) -> Unit,
|
onRecentSearchRemove: (String) -> Unit,
|
||||||
onClearAllRecentSearches: () -> Unit,
|
onClearAllRecentSearches: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val showRecentSearches = query.isBlank() && recentSearches.isNotEmpty()
|
|
||||||
val dropdownItems = remember(showRecentSearches, recentSearches) {
|
|
||||||
buildList {
|
|
||||||
if (showRecentSearches) {
|
|
||||||
add(SearchDropdownEntry.ClearAll(onClick = onClearAllRecentSearches))
|
|
||||||
recentSearches.forEach { search ->
|
|
||||||
add(
|
|
||||||
SearchDropdownEntry.RecentSearch(
|
|
||||||
query = search,
|
|
||||||
onClick = { onRecentSearchClick(search) },
|
|
||||||
onRemove = { onRecentSearchRemove(search) },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(bottom = 8.dp)
|
.padding(bottom = 8.dp)
|
||||||
) {
|
) {
|
||||||
AutocompleteTextField(
|
TextField(
|
||||||
value = query,
|
value = query,
|
||||||
onValueChange = onQueryChange,
|
onValueChange = onQueryChange,
|
||||||
items = dropdownItems,
|
singleLine = true,
|
||||||
onItemSelected = { entry -> entry.onClick() },
|
maxLines = 1,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
"Search songs, artists, albums...",
|
"Search songs, artists, albums...",
|
||||||
@ -440,77 +501,77 @@ private fun SearchBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
singleLine = true,
|
modifier = Modifier
|
||||||
modifier = Modifier.fillMaxWidth(),
|
.fillMaxWidth()
|
||||||
|
.onFocusChanged { onFocusChanged(it.isFocused) }
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.onKeyEvent(onKeyEvent),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
keyboardActions = KeyboardActions(onSearch = { onSearch() }),
|
keyboardActions = KeyboardActions(onSearch = { onSearch() }),
|
||||||
itemContent = { entry, isSelected ->
|
|
||||||
when (entry) {
|
|
||||||
is SearchDropdownEntry.ClearAll -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
|
||||||
.clip(RoundedCornerShape(4.dp))
|
|
||||||
.background(
|
|
||||||
if (isSelected) MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
else Color.Transparent
|
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
Row(
|
DropdownMenu(
|
||||||
|
expanded = showDropdown,
|
||||||
|
onDismissRequest = onDismissDropdown,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
.heightIn(max = 300.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
offset = DpOffset(x = 0.dp, y = 4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
properties = PopupProperties(focusable = false),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
shadowElevation = 8.dp,
|
||||||
) {
|
) {
|
||||||
|
Box(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||||
|
Column {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
"Clear all history",
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClearAllRecentSearches,
|
||||||
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.IconsaxTrash,
|
imageVector = Iconsax.IconsaxTrash,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
Text(
|
},
|
||||||
"Clear all history",
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is SearchDropdownEntry.RecentSearch -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.background(
|
.background(
|
||||||
if (isSelected) MaterialTheme.colorScheme.surfaceVariant
|
if (keyboardSelectedIndex == 0) MaterialTheme.colorScheme.surfaceVariant
|
||||||
else Color.Transparent
|
else MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
) {
|
)
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
recentSearches.forEachIndexed { index, search ->
|
||||||
.fillMaxWidth()
|
val isSelected = keyboardSelectedIndex == index + 1
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
DropdownMenuItem(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
text = {
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
Text(
|
||||||
) {
|
search,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.InconsaxClock,
|
imageVector = Iconsax.InconsaxClock,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Text(
|
},
|
||||||
entry.query,
|
trailingIcon = {
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = entry.onRemove,
|
onClick = { onRecentSearchRemove(search) },
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@ -520,12 +581,20 @@ private fun SearchBar(
|
|||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
onClick = { onRecentSearchClick(search) },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(
|
||||||
|
if (isSelected) MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
else MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,20 +21,12 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
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.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.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@ -46,30 +38,20 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.ripple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
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.unit.dp
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationState
|
import dev.krtirtho.spotube.core.navigation.NavigationState
|
||||||
import dev.krtirtho.spotube.core.navigation.Navigator
|
import dev.krtirtho.spotube.core.navigation.Navigator
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.buttonShadow
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.outlinedGradient
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
|
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryState
|
import dev.krtirtho.spotube.modules.library.LibraryState
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryTab
|
import dev.krtirtho.spotube.modules.library.LibraryTab
|
||||||
@ -174,7 +156,20 @@ fun SidebarItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
showDownloadBadge: Boolean = false,
|
showDownloadBadge: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val itemContent: @Composable RowScope.() -> Unit = {
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.background(
|
||||||
|
if (selected) MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
)
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = if (expanded) Arrangement.Start else Arrangement.Center
|
||||||
|
) {
|
||||||
Box(modifier = Modifier.size(24.dp)) {
|
Box(modifier = Modifier.size(24.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = activeIcon,
|
imageVector = activeIcon,
|
||||||
@ -183,7 +178,7 @@ fun SidebarItem(
|
|||||||
MaterialTheme.colorScheme.onSecondaryContainer
|
MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
if (showDownloadBadge) {
|
if (showDownloadBadge) {
|
||||||
DownloadBadgeIndicator(
|
DownloadBadgeIndicator(
|
||||||
@ -192,89 +187,17 @@ fun SidebarItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) {
|
AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
maxLines = 1,
|
|
||||||
softWrap = false,
|
|
||||||
color = if (selected) {
|
color = if (selected) {
|
||||||
MaterialTheme.colorScheme.onSecondaryContainer
|
MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(if (expanded) 1f else 0f))
|
|
||||||
}
|
|
||||||
|
|
||||||
val buttonModifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 10.dp, vertical = 4.dp)
|
|
||||||
val contentPadding = PaddingValues(horizontal = 14.dp, vertical = 12.dp)
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
SecondaryButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = buttonModifier,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
content = itemContent,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val colors = rememberButtonColors()
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
|
||||||
val gradient = outlinedGradient(colors, isPressed)
|
|
||||||
val border = colors.border.copy(alpha = if (isPressed) 0.7f else 1f)
|
|
||||||
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
|
||||||
val shape = RoundedCornerShape(14.dp)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = buttonModifier
|
|
||||||
.hoverable(interactionSource = interactionSource)
|
|
||||||
.graphicsLayer { translationY = lift.toPx() }
|
|
||||||
.clip(shape)
|
|
||||||
.then(
|
|
||||||
if (isHovered || isPressed) {
|
|
||||||
buttonShadow(
|
|
||||||
shape,
|
|
||||||
isPressed,
|
|
||||||
primary = false,
|
|
||||||
colors,
|
|
||||||
hovered = isHovered
|
|
||||||
).background(gradient, shape)
|
|
||||||
.border(BorderStroke(0.5.dp, border), shape)
|
|
||||||
.drawWithCache {
|
|
||||||
val highlightBrush = Brush.verticalGradient(
|
|
||||||
colors = listOf(colors.highlight, Color.Transparent),
|
|
||||||
startY = 0f,
|
|
||||||
endY = size.height * 0.5f,
|
|
||||||
)
|
|
||||||
onDrawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
brush = highlightBrush,
|
|
||||||
topLeft = androidx.compose.ui.geometry.Offset.Zero,
|
|
||||||
size = size,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.clickable(
|
}
|
||||||
interactionSource = interactionSource,
|
|
||||||
indication = ripple(),
|
|
||||||
onClick = onClick,
|
|
||||||
)
|
|
||||||
.padding(contentPadding),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
content = itemContent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user