diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts index 973681d9..619dfbb3 100644 --- a/app/ui/build.gradle.kts +++ b/app/ui/build.gradle.kts @@ -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", diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesTagSelector.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesTagSelector.kt new file mode 100644 index 00000000..d95399c8 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesTagSelector.kt @@ -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, + 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 + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt index eca8ecba..f891a740 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt @@ -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(null) val showEditButton = dataStore.data.map { it.favorites.editButton } + abstract val tagsExpanded: Flow 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) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index 6b0d155d..cf8a2a44 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -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 diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/favorites/SearchFavoritesVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/favorites/SearchFavoritesVM.kt index b49f2cd0..717b89ca 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/favorites/SearchFavoritesVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/favorites/SearchFavoritesVM.kt @@ -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() \ No newline at end of file +class SearchFavoritesVM : FavoritesVM() { + override val tagsExpanded: Flow = 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() + } + } + } + +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt index 087583c7..4820b886 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidget.kt @@ -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) } + ) } } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt index de2b3815..8e00f29c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/favorites/FavoritesWidgetVM.kt @@ -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() \ No newline at end of file +class FavoritesWidgetVM: FavoritesVM() { + override val tagsExpanded: Flow = 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() + } + } + } + +} \ No newline at end of file diff --git a/core/preferences/src/main/proto/settings.proto b/core/preferences/src/main/proto/settings.proto index 6f23b1c3..40541a4e 100644 --- a/core/preferences/src/main/proto/settings.proto +++ b/core/preferences/src/main/proto/settings.proto @@ -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; } \ No newline at end of file