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.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.ui.text.ExperimentalTextApi", "-opt-in=androidx.compose.ui.text.ExperimentalTextApi",
"-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi", "-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi", "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", "-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.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
open class FavoritesVM : ViewModel(), KoinComponent { abstract class FavoritesVM : ViewModel(), KoinComponent {
private val favoritesRepository: FavoritesRepository by inject() private val favoritesRepository: FavoritesRepository by inject()
private val widgetRepository: WidgetRepository by inject() private val widgetRepository: WidgetRepository by inject()
private val customAttributesRepository: CustomAttributesRepository 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 selectedTag = MutableStateFlow<String?>(null)
val showEditButton = dataStore.data.map { it.favorites.editButton } val showEditButton = dataStore.data.map { it.favorites.editButton }
abstract val tagsExpanded: Flow<Boolean>
val pinnedTags = favoritesRepository.getFavorites( val pinnedTags = favoritesRepository.getFavorites(
includeTypes = listOf("tag"), includeTypes = listOf("tag"),
@ -88,4 +89,6 @@ open class FavoritesVM : ViewModel(), KoinComponent {
fun selectTag(tag: String?) { fun selectTag(tag: String?) {
selectedTag.value = tag 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.horizontalScroll
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.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -17,6 +18,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit 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.Person
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Tag 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.FilterChip
import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
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.remember
@ -44,6 +48,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R 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.Banner
import de.mm20.launcher2.ui.component.LauncherCard import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -108,6 +113,7 @@ fun SearchColumn(
val selectedTag by favoritesVM.selectedTag.collectAsState(null) val selectedTag by favoritesVM.selectedTag.collectAsState(null)
val tagsScrollState = rememberScrollState() val tagsScrollState = rememberScrollState()
val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false) val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false)
val favoritesTagsExpanded by favoritesVM.tagsExpanded.collectAsState(false)
LazyColumn( LazyColumn(
state = state, state = state,
@ -136,63 +142,16 @@ fun SearchColumn(
null null
} else { } else {
{ {
Row( FavoritesTagSelector(
modifier = Modifier tags = pinnedTags,
.fillMaxWidth() selectedTag = selectedTag,
.padding( editButton = favoritesEditButton,
top = if (reverse) 8.dp else 4.dp, reverse = reverse,
bottom = if (reverse) 4.dp else 8.dp, onSelectTag = { favoritesVM.selectTag(it) },
end = if (favoritesEditButton) 8.dp else 0.dp scrollState = tagsScrollState,
), expanded = favoritesTagsExpanded,
horizontalArrangement = Arrangement.End, onExpand = { favoritesVM.setTagsExpanded(it) }
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
)
}
}
}
} }
}, },
highlightedItem = bestMatch as? SavableSearchable highlightedItem = bestMatch as? SavableSearchable

View File

@ -1,5 +1,29 @@
package de.mm20.launcher2.ui.launcher.search.favorites package de.mm20.launcher2.ui.launcher.search.favorites
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ui.common.FavoritesVM 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 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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons 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.Star
import androidx.compose.material.icons.rounded.Tag 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R 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.Banner
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
@Composable @Composable
fun FavoritesWidget() { fun FavoritesWidget() {
@ -40,6 +27,8 @@ fun FavoritesWidget() {
val selectedTag by viewModel.selectedTag.collectAsState(null) val selectedTag by viewModel.selectedTag.collectAsState(null)
val favoritesEditButton by viewModel.showEditButton.collectAsState(false) val favoritesEditButton by viewModel.showEditButton.collectAsState(false)
val tagsExpanded by viewModel.tagsExpanded.collectAsState(false)
Column { Column {
if (favorites.isNotEmpty()) { if (favorites.isNotEmpty()) {
SearchResultGrid(favorites) SearchResultGrid(favorites)
@ -53,60 +42,16 @@ fun FavoritesWidget() {
) )
} }
if (pinnedTags.isNotEmpty() || favoritesEditButton) { if (pinnedTags.isNotEmpty() || favoritesEditButton) {
Row( FavoritesTagSelector(
modifier = Modifier tags = pinnedTags,
.fillMaxWidth() selectedTag = selectedTag,
.padding( editButton = favoritesEditButton,
top = 4.dp, reverse = false,
bottom = 8.dp, onSelectTag = { viewModel.selectTag(it) },
end = if (favoritesEditButton) 8.dp else 0.dp scrollState = rememberScrollState(),
), expanded = tagsExpanded,
horizontalArrangement = Arrangement.End, onExpand = { viewModel.setTagsExpanded(it) }
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)
}
}
}
} }
} }
} }

View File

@ -1,5 +1,29 @@
package de.mm20.launcher2.ui.launcher.widgets.favorites package de.mm20.launcher2.ui.launcher.widgets.favorites
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ui.common.FavoritesVM 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; WeightFactor weight_factor = 2;
} }
SearchResultOrderingSettings result_ordering = 29; 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;
} }