Add ability to expand pinned tags to multiple lines

This commit is contained in:
MM20 2023-03-11 00:26:02 +01:00
parent eae063879c
commit c37b280d29
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
8 changed files with 261 additions and 129 deletions

View File

@ -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",

View File

@ -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
)
}
}
}
}
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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()
}
}
}
}

View File

@ -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) }
)
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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;
}