Add ability to expand pinned tags to multiple lines
This commit is contained in:
parent
eae063879c
commit
c37b280d29
@ -39,6 +39,7 @@ android {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.ui.text.ExperimentalTextApi",
|
||||
"-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
package de.mm20.launcher2.ui.common
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.ExpandLess
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.search.data.Tag
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
|
||||
@Composable
|
||||
fun FavoritesTagSelector(
|
||||
tags: List<Tag>,
|
||||
selectedTag: String?,
|
||||
editButton: Boolean,
|
||||
reverse: Boolean,
|
||||
onSelectTag: (String?) -> Unit,
|
||||
scrollState: ScrollState,
|
||||
expanded: Boolean,
|
||||
onExpand: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
.padding(
|
||||
top = if (reverse) 8.dp else 4.dp,
|
||||
bottom = if (reverse) 4.dp else 8.dp,
|
||||
end = if (editButton) 8.dp else 0.dp
|
||||
)
|
||||
then
|
||||
if (editButton && expanded) Modifier.height(IntrinsicSize.Min) else Modifier,
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (!expanded) {
|
||||
val canScroll by remember {
|
||||
derivedStateOf { scrollState.canScrollForward || scrollState.canScrollBackward }
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.horizontalScroll(scrollState)
|
||||
.padding(end = 12.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
selected = selectedTag == null,
|
||||
onClick = { onSelectTag(null) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Star,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(R.string.favorites)) }
|
||||
)
|
||||
for (tag in tags) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
selected = selectedTag == tag.tag,
|
||||
onClick = { onSelectTag(tag.tag) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Tag,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(tag.label) }
|
||||
)
|
||||
}
|
||||
if (canScroll) {
|
||||
IconButton(
|
||||
onClick = { onExpand(true) }) {
|
||||
Icon(Icons.Rounded.ExpandMore, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 12.dp, start = 16.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
selected = selectedTag == null,
|
||||
onClick = { onSelectTag(null) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Star,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(R.string.favorites)) }
|
||||
)
|
||||
for (tag in tags) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
selected = selectedTag == tag.tag,
|
||||
onClick = { onSelectTag(tag.tag) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Tag,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(tag.label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (editButton || expanded) {
|
||||
Column(
|
||||
modifier = if (expanded && editButton) Modifier.fillMaxHeight() else Modifier,
|
||||
verticalArrangement = if (expanded && editButton) Arrangement.SpaceBetween else Arrangement.Center,
|
||||
) {
|
||||
if (expanded) {
|
||||
IconButton(onClick = { onExpand(false) }) {
|
||||
Icon(Icons.Rounded.ExpandLess, null)
|
||||
}
|
||||
}
|
||||
if (editButton) {
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
SmallFloatingActionButton(
|
||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
|
||||
onClick = { sheetManager.showEditFavoritesSheet() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Edit,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,16 +13,17 @@ import kotlinx.coroutines.flow.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
open class FavoritesVM : ViewModel(), KoinComponent {
|
||||
abstract class FavoritesVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val favoritesRepository: FavoritesRepository by inject()
|
||||
private val widgetRepository: WidgetRepository by inject()
|
||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
internal val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val selectedTag = MutableStateFlow<String?>(null)
|
||||
|
||||
val showEditButton = dataStore.data.map { it.favorites.editButton }
|
||||
abstract val tagsExpanded: Flow<Boolean>
|
||||
|
||||
val pinnedTags = favoritesRepository.getFavorites(
|
||||
includeTypes = listOf("tag"),
|
||||
@ -88,4 +89,6 @@ open class FavoritesVM : ViewModel(), KoinComponent {
|
||||
fun selectTag(tag: String?) {
|
||||
selectedTag.value = tag
|
||||
}
|
||||
|
||||
abstract fun setTagsExpanded(expanded: Boolean)
|
||||
}
|
||||
@ -4,6 +4,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@ -17,6 +18,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
@ -24,12 +26,14 @@ import androidx.compose.material.icons.rounded.Work
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -44,6 +48,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.common.FavoritesTagSelector
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
@ -108,6 +113,7 @@ fun SearchColumn(
|
||||
val selectedTag by favoritesVM.selectedTag.collectAsState(null)
|
||||
val tagsScrollState = rememberScrollState()
|
||||
val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false)
|
||||
val favoritesTagsExpanded by favoritesVM.tagsExpanded.collectAsState(false)
|
||||
|
||||
LazyColumn(
|
||||
state = state,
|
||||
@ -136,63 +142,16 @@ fun SearchColumn(
|
||||
null
|
||||
} else {
|
||||
{
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = if (reverse) 8.dp else 4.dp,
|
||||
bottom = if (reverse) 4.dp else 8.dp,
|
||||
end = if (favoritesEditButton) 8.dp else 0.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.horizontalScroll(tagsScrollState)
|
||||
.padding(end = 12.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
selected = selectedTag == null,
|
||||
onClick = { favoritesVM.selectTag(null) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Star,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(R.string.favorites)) }
|
||||
)
|
||||
for (tag in pinnedTags) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
selected = selectedTag == tag.tag,
|
||||
onClick = { favoritesVM.selectTag(tag.tag) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Tag,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(tag.label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (favoritesEditButton) {
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
SmallFloatingActionButton(
|
||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
|
||||
onClick = { sheetManager.showEditFavoritesSheet() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Edit,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FavoritesTagSelector(
|
||||
tags = pinnedTags,
|
||||
selectedTag = selectedTag,
|
||||
editButton = favoritesEditButton,
|
||||
reverse = reverse,
|
||||
onSelectTag = { favoritesVM.selectTag(it) },
|
||||
scrollState = tagsScrollState,
|
||||
expanded = favoritesTagsExpanded,
|
||||
onExpand = { favoritesVM.setTagsExpanded(it) }
|
||||
)
|
||||
}
|
||||
},
|
||||
highlightedItem = bestMatch as? SavableSearchable
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
package de.mm20.launcher2.ui.launcher.search.favorites
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.ui.common.FavoritesVM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SearchFavoritesVM: FavoritesVM()
|
||||
class SearchFavoritesVM : FavoritesVM() {
|
||||
override val tagsExpanded: Flow<Boolean> = dataStore.data.map { it.ui.searchTagsMultiline }
|
||||
.shareIn(viewModelScope, SharingStarted.Lazily)
|
||||
|
||||
override fun setTagsExpanded(expanded: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setUi(
|
||||
it.ui.toBuilder()
|
||||
.setSearchTagsMultiline(expanded)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,36 +1,23 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets.favorites
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.common.FavoritesTagSelector
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||
|
||||
@Composable
|
||||
fun FavoritesWidget() {
|
||||
@ -40,6 +27,8 @@ fun FavoritesWidget() {
|
||||
val selectedTag by viewModel.selectedTag.collectAsState(null)
|
||||
val favoritesEditButton by viewModel.showEditButton.collectAsState(false)
|
||||
|
||||
val tagsExpanded by viewModel.tagsExpanded.collectAsState(false)
|
||||
|
||||
Column {
|
||||
if (favorites.isNotEmpty()) {
|
||||
SearchResultGrid(favorites)
|
||||
@ -53,60 +42,16 @@ fun FavoritesWidget() {
|
||||
)
|
||||
}
|
||||
if (pinnedTags.isNotEmpty() || favoritesEditButton) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = 4.dp,
|
||||
bottom = 8.dp,
|
||||
end = if (favoritesEditButton) 8.dp else 0.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(end = 12.dp),
|
||||
) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
selected = selectedTag == null,
|
||||
onClick = { viewModel.selectTag(null) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Star,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(R.string.favorites)) }
|
||||
)
|
||||
for (tag in pinnedTags) {
|
||||
FilterChip(
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
selected = selectedTag == tag.tag,
|
||||
onClick = { viewModel.selectTag(tag.tag) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Tag,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
label = { Text(tag.label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (favoritesEditButton) {
|
||||
val bottomSheetManager = LocalBottomSheetManager.current
|
||||
SmallFloatingActionButton(
|
||||
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
|
||||
onClick = { bottomSheetManager.showEditFavoritesSheet() }
|
||||
) {
|
||||
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
FavoritesTagSelector(
|
||||
tags = pinnedTags,
|
||||
selectedTag = selectedTag,
|
||||
editButton = favoritesEditButton,
|
||||
reverse = false,
|
||||
onSelectTag = { viewModel.selectTag(it) },
|
||||
scrollState = rememberScrollState(),
|
||||
expanded = tagsExpanded,
|
||||
onExpand = { viewModel.setTagsExpanded(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,29 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets.favorites
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.ui.common.FavoritesVM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FavoritesWidgetVM: FavoritesVM()
|
||||
class FavoritesWidgetVM: FavoritesVM() {
|
||||
override val tagsExpanded: Flow<Boolean> = dataStore.data.map { it.ui.widgetTagsMultiline }
|
||||
.shareIn(viewModelScope, SharingStarted.Lazily)
|
||||
|
||||
override fun setTagsExpanded(expanded: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setUi(
|
||||
it.ui.toBuilder()
|
||||
.setWidgetTagsMultiline(expanded)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -343,4 +343,13 @@ message Settings {
|
||||
WeightFactor weight_factor = 2;
|
||||
}
|
||||
SearchResultOrderingSettings result_ordering = 29;
|
||||
|
||||
/**
|
||||
* Persistent UI state that does not have a corresponding setting.
|
||||
*/
|
||||
message UiState {
|
||||
bool search_tags_multiline = 1;
|
||||
bool widget_tags_multiline = 2;
|
||||
}
|
||||
UiState ui = 30;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user