Add search quick actions
This commit is contained in:
@@ -141,4 +141,5 @@ dependencies {
|
||||
implementation(project(":owncloud"))
|
||||
implementation(project(":accounts"))
|
||||
implementation(project(":backup"))
|
||||
implementation(project(":search-actions"))
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
@@ -22,12 +21,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
|
||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -162,9 +162,9 @@ fun AssistantScaffold(
|
||||
}
|
||||
|
||||
val searchVM: SearchVM = viewModel()
|
||||
val websearches by searchVM.websearchResults.observeAsState(emptyList())
|
||||
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||
val webSearchPadding by animateDpAsState(
|
||||
if (websearches.isEmpty()) 0.dp else 48.dp
|
||||
if (actions.isEmpty()) 0.dp else 48.dp
|
||||
)
|
||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
Box(
|
||||
@@ -182,8 +182,12 @@ fun AssistantScaffold(
|
||||
state = searchState
|
||||
)
|
||||
|
||||
SearchBar(
|
||||
level = { searchBarLevel },
|
||||
val value by searchVM.searchQuery.observeAsState("")
|
||||
|
||||
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
||||
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
||||
|
||||
LauncherSearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
@@ -197,10 +201,17 @@ fun AssistantScaffold(
|
||||
searchBarOffset.toInt() * if (bottomSearchBar == true) -1 else 1
|
||||
)
|
||||
},
|
||||
level = { searchBarLevel },
|
||||
focused = searchBarFocused,
|
||||
onFocusChange = {
|
||||
if (it) viewModel.openSearch()
|
||||
viewModel.setSearchbarFocus(it)
|
||||
},
|
||||
actions = actions,
|
||||
value = { value },
|
||||
onValueChange = { searchVM.search(it) },
|
||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
||||
style = searchBarStyle,
|
||||
reverse = bottomSearchBar == true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ fun LauncherCard(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
||||
shadowElevation = if (backgroundOpacity == 1f) elevation else 0.dp,
|
||||
tonalElevation = elevation
|
||||
tonalElevation = elevation,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
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.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
style: Settings.SearchBarSettings.SearchBarStyle,
|
||||
level: SearchBarLevel,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
onFocus: () -> Unit = {},
|
||||
onUnfocus: () -> Unit = {},
|
||||
reverse: Boolean = false,
|
||||
darkColors: Boolean = false,
|
||||
menu: @Composable RowScope.() -> Unit = {},
|
||||
actions: @Composable () -> Unit = {},
|
||||
) {
|
||||
|
||||
val transition = updateTransition(level, label = "Searchbar")
|
||||
|
||||
val elevation by transition.animateDp(
|
||||
label = "elevation",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
targetState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}
|
||||
) {
|
||||
when {
|
||||
it == SearchBarLevel.Resting && style != Settings.SearchBarSettings.SearchBarStyle.Solid -> 0.dp
|
||||
it == SearchBarLevel.Raised -> 8.dp
|
||||
else -> 2.dp
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundOpacity by transition.animateFloat(label = "backgroundOpacity",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
else -> tween(durationMillis = 200)
|
||||
}
|
||||
}) {
|
||||
when {
|
||||
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
|
||||
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> 1f
|
||||
it == SearchBarLevel.Resting -> 0f
|
||||
else -> 1f
|
||||
}
|
||||
}
|
||||
|
||||
val contentColor by transition.animateColor(label = "textColor",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}) {
|
||||
when {
|
||||
style != Settings.SearchBarSettings.SearchBarStyle.Transparent -> MaterialTheme.colorScheme.onSurface
|
||||
it == SearchBarLevel.Resting -> if (darkColors) Color(0, 0, 0, 180) else Color.White
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
|
||||
val opacity by transition.animateFloat(label = "opacity") {
|
||||
if (style == Settings.SearchBarSettings.SearchBarStyle.Hidden && it == SearchBarLevel.Resting) 0f
|
||||
else 1f
|
||||
}
|
||||
|
||||
LauncherCard(
|
||||
modifier = modifier
|
||||
.alpha(opacity),
|
||||
backgroundOpacity = backgroundOpacity,
|
||||
elevation = elevation
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides contentColor
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
imageVector = androidx.compose.material.icons.Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = contentColor
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.search_bar_placeholder),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
LaunchedEffect(level) {
|
||||
if (level == SearchBarLevel.Resting) onUnfocus()
|
||||
}
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.onFocusChanged {
|
||||
if (it.hasFocus) onFocus()
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.titleMedium.copy(
|
||||
color = contentColor
|
||||
),
|
||||
singleLine = true,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
menu()
|
||||
}
|
||||
}
|
||||
actions()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SearchBarLevel {
|
||||
/**
|
||||
* The default, "hidden" state, when the launcher is in its initial state (scroll position is 0
|
||||
* and search is closed)
|
||||
*/
|
||||
Resting,
|
||||
|
||||
/**
|
||||
* When the search is open but there is no content behind the search bar (scroll position is 0)
|
||||
*/
|
||||
Active,
|
||||
|
||||
/**
|
||||
* When there is content below the search bar which requires the search bar to be raised above
|
||||
* this content (scroll position is not 0)
|
||||
*/
|
||||
Raised
|
||||
}
|
||||
@@ -52,4 +52,6 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
||||
val wallpaperBlur = dataStore.data.map { it.appearance.blurWallpaper }.asLiveData()
|
||||
|
||||
val fillClockHeight = dataStore.data.map { it.clockWidget.fillHeight }.asLiveData()
|
||||
val searchBarColor = dataStore.data.map { it.searchBar.color }.asLiveData()
|
||||
val searchBarStyle = dataStore.data.map { it.searchBar.searchBarStyle }.asLiveData()
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
@@ -8,7 +7,24 @@ import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.LocalOverscrollConfiguration
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -17,9 +33,19 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material.rememberSwipeableState
|
||||
import androidx.compose.material.swipeable
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -37,15 +63,18 @@ import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import de.mm20.launcher2.ui.utils.rememberNotificationShadeController
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
@@ -64,6 +93,8 @@ fun PagerScaffold(
|
||||
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
||||
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
||||
|
||||
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||
|
||||
val widgetsScrollState = rememberScrollState()
|
||||
val searchState = rememberLazyListState()
|
||||
val swipeableState = rememberSwipeableState(if (isSearchOpen) Page.Search else Page.Widgets)
|
||||
@@ -76,7 +107,8 @@ fun PagerScaffold(
|
||||
|
||||
val isSearchAtEnd by remember {
|
||||
derivedStateOf {
|
||||
val lastItem = searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
||||
val lastItem =
|
||||
searchState.layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf true
|
||||
lastItem.offset + lastItem.size <= searchState.layoutInfo.viewportEndOffset - searchState.layoutInfo.afterContentPadding
|
||||
}
|
||||
}
|
||||
@@ -179,9 +211,11 @@ fun PagerScaffold(
|
||||
viewModel.closeSearch()
|
||||
searchVM.search("")
|
||||
}
|
||||
|
||||
isWidgetEditMode -> {
|
||||
viewModel.setWidgetEditMode(false)
|
||||
}
|
||||
|
||||
widgetsScrollState.value != 0 -> {
|
||||
scope.launch {
|
||||
widgetsScrollState.animateScrollTo(0)
|
||||
@@ -226,14 +260,16 @@ fun PagerScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
val searchNestedScrollConnection = remember { object: NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
if (source == NestedScrollSource.Drag && available.y.absoluteValue > available.x.absoluteValue * 2) {
|
||||
keyboardController?.hide()
|
||||
val searchNestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
if (source == NestedScrollSource.Drag && available.y.absoluteValue > available.x.absoluteValue * 2) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
return super.onPreScroll(available, source)
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
val insets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
|
||||
@@ -296,7 +332,7 @@ fun PagerScaffold(
|
||||
|
||||
val clockHeight by remember {
|
||||
derivedStateOf {
|
||||
if (fillClockHeight){
|
||||
if (fillClockHeight) {
|
||||
height - (64.dp + insets.calculateTopPadding() + insets.calculateBottomPadding() - clockPadding)
|
||||
} else {
|
||||
null
|
||||
@@ -338,10 +374,8 @@ fun PagerScaffold(
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val websearches by searchVM.websearchResults.observeAsState(emptyList())
|
||||
val webSearchPadding by animateDpAsState(
|
||||
if (websearches.isEmpty()) 0.dp else 48.dp
|
||||
if (actions.isEmpty()) 0.dp else 48.dp
|
||||
)
|
||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
SearchColumn(
|
||||
@@ -398,17 +432,29 @@ fun PagerScaffold(
|
||||
if (isWidgetEditMode) 128.dp else 0.dp
|
||||
)
|
||||
|
||||
SearchBar(
|
||||
val value by searchVM.searchQuery.observeAsState("")
|
||||
|
||||
val searchBarColor by viewModel.searchBarColor.observeAsState(SearchBarColors.Auto)
|
||||
val searchBarStyle by viewModel.searchBarStyle.observeAsState(SearchBarStyle.Transparent)
|
||||
|
||||
LauncherSearchBar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.imePadding()
|
||||
.offset(y = widgetEditModeOffset),
|
||||
level = { searchBarLevel }, focused = focusSearchBar, onFocusChange = {
|
||||
level = { searchBarLevel },
|
||||
focused = focusSearchBar,
|
||||
onFocusChange = {
|
||||
if (it) viewModel.openSearch()
|
||||
viewModel.setSearchbarFocus(it)
|
||||
},
|
||||
actions = actions,
|
||||
value = { value },
|
||||
onValueChange = { searchVM.search(it) },
|
||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
|
||||
style = searchBarStyle,
|
||||
reverse = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,15 +34,17 @@ import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.ktx.animateTo
|
||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
|
||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
@@ -58,6 +60,8 @@ fun PullDownScaffold(
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val actions by searchVM.searchActionResults.observeAsState(emptyList())
|
||||
|
||||
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
||||
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
||||
|
||||
@@ -277,9 +281,8 @@ fun PullDownScaffold(
|
||||
)
|
||||
}
|
||||
) {
|
||||
val websearches by searchVM.websearchResults.observeAsState(emptyList())
|
||||
val webSearchPadding by animateDpAsState(
|
||||
if (websearches.isEmpty()) 0.dp else 48.dp
|
||||
if (actions.isEmpty()) 0.dp else 48.dp
|
||||
)
|
||||
val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
|
||||
SearchColumn(
|
||||
@@ -392,8 +395,12 @@ fun PullDownScaffold(
|
||||
if (isWidgetEditMode) -128.dp else 0.dp
|
||||
)
|
||||
|
||||
SearchBar(
|
||||
level = { searchBarLevel },
|
||||
val value by searchVM.searchQuery.observeAsState("")
|
||||
|
||||
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
||||
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
||||
|
||||
LauncherSearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
@@ -410,11 +417,17 @@ fun PullDownScaffold(
|
||||
.roundToInt()
|
||||
})
|
||||
},
|
||||
level = { searchBarLevel },
|
||||
focused = searchBarFocused,
|
||||
onFocusChange = {
|
||||
if (it) viewModel.openSearch()
|
||||
viewModel.setSearchbarFocus(it)
|
||||
}
|
||||
},
|
||||
actions = actions,
|
||||
value = { value },
|
||||
onValueChange = { searchVM.search(it) },
|
||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
||||
style = searchBarStyle,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.HelpOutline
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Wallpaper
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedAssistChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
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.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.launcher.LauncherActivityVM
|
||||
import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
|
||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koin.androidx.compose.inject
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
level: () -> SearchBarLevel,
|
||||
focused: Boolean,
|
||||
onFocusChange: (Boolean) -> Unit,
|
||||
reverse: Boolean = false,
|
||||
) {
|
||||
val searchViewModel: SearchVM = viewModel()
|
||||
val activityViewModel: LauncherActivityVM = viewModel()
|
||||
|
||||
val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val style by remember { dataStore.data.map { it.searchBar.searchBarStyle } }
|
||||
.collectAsState(SearchBarSettings.SearchBarStyle.Hidden)
|
||||
|
||||
val color by remember { dataStore.data.map { it.searchBar.color } }
|
||||
.collectAsState(SearchBarSettings.SearchBarColors.Auto)
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(focused) {
|
||||
if (focused) focusRequester.requestFocus()
|
||||
else focusManager.clearFocus()
|
||||
}
|
||||
|
||||
val query by searchViewModel.searchQuery.observeAsState("")
|
||||
|
||||
val websearches by searchViewModel.websearchResults.observeAsState(emptyList())
|
||||
|
||||
SearchBar(
|
||||
modifier,
|
||||
level(),
|
||||
websearches,
|
||||
value = query,
|
||||
onValueChange = {
|
||||
searchViewModel.search(it)
|
||||
},
|
||||
style = style,
|
||||
overflowMenu = { show, onDismissRequest ->
|
||||
DropdownMenu(expanded = show, onDismissRequest = onDismissRequest) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_SET_WALLPAPER),
|
||||
null
|
||||
)
|
||||
)
|
||||
onDismissRequest()
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.wallpaper))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||
onDismissRequest()
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.settings))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Settings, contentDescription = null)
|
||||
}
|
||||
)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(colorScheme.primaryContainer.toArgb())
|
||||
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
|
||||
.build()
|
||||
)
|
||||
.build().launchUrl(context, Uri.parse("https://kvaesitso.mm20.de/docs/user-guide"))
|
||||
onDismissRequest()
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.help))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.HelpOutline, contentDescription = null)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
focusRequester = focusRequester,
|
||||
onFocus = {
|
||||
onFocusChange(true)
|
||||
},
|
||||
onUnfocus = {
|
||||
onFocusChange(false)
|
||||
},
|
||||
reverse = reverse,
|
||||
darkColors = color == SearchBarColors.Dark || color == SearchBarColors.Auto && LocalPreferDarkContentOverWallpaper.current
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
level: SearchBarLevel,
|
||||
websearches: List<Websearch>,
|
||||
overflowMenu: @Composable (show: Boolean, onDismissRequest: () -> Unit) -> Unit = { _, _ -> },
|
||||
value: String,
|
||||
style: SearchBarSettings.SearchBarStyle,
|
||||
onValueChange: (String) -> Unit,
|
||||
onFocus: () -> Unit = {},
|
||||
onUnfocus: () -> Unit = {},
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
reverse: Boolean = false,
|
||||
darkColors: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||
|
||||
val transition = updateTransition(level, label = "Searchbar")
|
||||
|
||||
|
||||
val elevation by transition.animateDp(
|
||||
label = "elevation",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
targetState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}
|
||||
) {
|
||||
when {
|
||||
it == SearchBarLevel.Resting && style != SearchBarSettings.SearchBarStyle.Solid -> 0.dp
|
||||
it == SearchBarLevel.Raised -> 8.dp
|
||||
else -> 2.dp
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundOpacity by transition.animateFloat(label = "backgroundOpacity",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
else -> tween(durationMillis = 200)
|
||||
}
|
||||
}) {
|
||||
when {
|
||||
it == SearchBarLevel.Active -> LocalCardStyle.current.opacity
|
||||
style != SearchBarSettings.SearchBarStyle.Transparent -> 1f
|
||||
it == SearchBarLevel.Resting -> 0f
|
||||
else -> 1f
|
||||
}
|
||||
}
|
||||
|
||||
val contentColor by transition.animateColor(label = "textColor",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}) {
|
||||
when {
|
||||
style != SearchBarSettings.SearchBarStyle.Transparent -> MaterialTheme.colorScheme.onSurface
|
||||
it == SearchBarLevel.Resting -> if (darkColors) Color(0, 0, 0, 180) else Color.White
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
}
|
||||
|
||||
val opacity by transition.animateFloat(label = "opacity") {
|
||||
if (style == SearchBarSettings.SearchBarStyle.Hidden && it == SearchBarLevel.Resting) 0f
|
||||
else 1f
|
||||
}
|
||||
|
||||
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
||||
|
||||
LauncherCard(
|
||||
modifier = modifier
|
||||
.alpha(opacity),
|
||||
backgroundOpacity = backgroundOpacity,
|
||||
elevation = elevation
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = contentColor
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.search_bar_placeholder),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
LaunchedEffect(level) {
|
||||
if (level == SearchBarLevel.Resting) onUnfocus()
|
||||
}
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.onFocusChanged {
|
||||
if (it.hasFocus) onFocus()
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = contentColor
|
||||
),
|
||||
singleLine = true,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
)
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = {
|
||||
if (value.isNotBlank()) onValueChange("")
|
||||
else showOverflowMenu = true
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(
|
||||
rightIcon,
|
||||
atEnd = value.isNotBlank()
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = contentColor
|
||||
)
|
||||
}
|
||||
overflowMenu(showOverflowMenu) { showOverflowMenu = false }
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(websearches.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.padding(bottom = 12.dp, top = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||
) {
|
||||
items(websearches) {
|
||||
AssistChip(
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
onClick = {
|
||||
it
|
||||
.getLaunchIntent()
|
||||
?.let {
|
||||
context.tryStartActivity(it)
|
||||
}
|
||||
},
|
||||
label = { Text(it.label) },
|
||||
leadingIcon = {
|
||||
val icon = it.icon
|
||||
if (icon == null) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = if (it.color == 0) MaterialTheme.colorScheme.primary else Color(
|
||||
it.color
|
||||
)
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
modifier = Modifier.size(24.dp),
|
||||
model = File(icon),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SearchBarLevel {
|
||||
/**
|
||||
* The default, "hidden" state, when the launcher is in its initial state (scroll position is 0
|
||||
* and search is closed)
|
||||
*/
|
||||
Resting,
|
||||
|
||||
/**
|
||||
* When the search is open but there is no content behind the search bar (scroll position is 0)
|
||||
*/
|
||||
Active,
|
||||
|
||||
/**
|
||||
* When there is content below the search bar which requires the search bar to be raised above
|
||||
* this content (scroll position is not 0)
|
||||
*/
|
||||
Raised
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package de.mm20.launcher2.ui.launcher.search
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
@@ -12,9 +11,25 @@ import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.WebsearchRepository
|
||||
import de.mm20.launcher2.search.data.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.Calculator
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.LauncherApp
|
||||
import de.mm20.launcher2.search.data.UnitConverter
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
@@ -41,7 +56,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
val websiteResults = MutableLiveData<List<Website>>(emptyList())
|
||||
val calculatorResults = MutableLiveData<List<Calculator>>(emptyList())
|
||||
val unitConverterResults = MutableLiveData<List<UnitConverter>>(emptyList())
|
||||
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
|
||||
val searchActionResults = MutableLiveData<List<SearchAction>>(emptyList())
|
||||
|
||||
val hiddenResults = MutableLiveData<List<SavableSearchable>>(emptyList())
|
||||
|
||||
@@ -71,9 +86,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
searchJob = viewModelScope.launch {
|
||||
isSearching.postValue(true)
|
||||
|
||||
websearchResults.value = websearchRepository.search(query).first()
|
||||
|
||||
|
||||
dataStore.data.collectLatest {
|
||||
searchService.search(
|
||||
query,
|
||||
@@ -85,6 +97,7 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
shortcuts = it.appShortcutSearch,
|
||||
websites = it.websiteSearch,
|
||||
wikipedia = it.wikipediaSearch,
|
||||
searchActions = it.searchActions,
|
||||
).collectLatest { results ->
|
||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
||||
val hidden = mutableListOf<SavableSearchable>()
|
||||
@@ -98,11 +111,13 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
val calc = mutableListOf<Calculator>()
|
||||
val wikipedia = mutableListOf<Wikipedia>()
|
||||
val website = mutableListOf<Website>()
|
||||
val actions = mutableListOf<SearchAction>()
|
||||
for (r in results) {
|
||||
when {
|
||||
r is SavableSearchable && hiddenKeys.contains(r.key) -> {
|
||||
hidden.add(r)
|
||||
}
|
||||
|
||||
r is LauncherApp && !r.isMainProfile -> workApps.add(r)
|
||||
r is LauncherApp -> apps.add(r)
|
||||
r is AppShortcut -> shortcuts.add(r)
|
||||
@@ -113,8 +128,10 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
r is Calculator -> calc.add(r)
|
||||
r is Website -> website.add(r)
|
||||
r is Wikipedia -> wikipedia.add(r)
|
||||
r is SearchAction -> actions.add(r)
|
||||
}
|
||||
}
|
||||
searchActionResults.value = actions
|
||||
appResults.value = apps
|
||||
workAppResults.value = workApps
|
||||
appShortcutResults.value = shortcuts
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package de.mm20.launcher2.ui.launcher.searchbar
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.ui.component.SearchBar
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
|
||||
@Composable
|
||||
fun LauncherSearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
style: Settings.SearchBarSettings.SearchBarStyle,
|
||||
level: () -> SearchBarLevel,
|
||||
value: () -> String,
|
||||
onValueChange: (String) -> Unit,
|
||||
focused: Boolean,
|
||||
onFocusChange: (Boolean) -> Unit,
|
||||
actions: List<SearchAction>,
|
||||
reverse: Boolean = false,
|
||||
darkColors: Boolean = false,
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
|
||||
LaunchedEffect(focused) {
|
||||
if (focused) focusRequester.requestFocus()
|
||||
else focusManager.clearFocus()
|
||||
}
|
||||
|
||||
val _value = value()
|
||||
|
||||
SearchBar(
|
||||
modifier = modifier,
|
||||
style = style, level = level(), value = _value, onValueChange = onValueChange,
|
||||
reverse = reverse,
|
||||
darkColors = darkColors,
|
||||
menu = {
|
||||
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
|
||||
},
|
||||
actions = {
|
||||
SearchBarActions(actions = actions, reverse = reverse)
|
||||
},
|
||||
focusRequester = focusRequester,
|
||||
onFocus = { onFocusChange(true) },
|
||||
onUnfocus = { onFocusChange(false) },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package de.mm20.launcher2.ui.launcher.searchbar
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Alarm
|
||||
import androidx.compose.material.icons.rounded.Call
|
||||
import androidx.compose.material.icons.rounded.Email
|
||||
import androidx.compose.material.icons.rounded.Event
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Sms
|
||||
import androidx.compose.material.icons.rounded.Timer
|
||||
import androidx.compose.material.icons.rounded.Translate
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
@Composable
|
||||
fun SearchBarActions(
|
||||
modifier: Modifier = Modifier,
|
||||
actions: List<SearchAction>,
|
||||
reverse: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
AnimatedVisibility(actions.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.padding(bottom = if (reverse) 4.dp else 12.dp, top = if (reverse) 12.dp else 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||
) {
|
||||
items(actions) {
|
||||
AssistChip(
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
onClick = {
|
||||
it.start(context)
|
||||
},
|
||||
label = { Text(it.label) },
|
||||
leadingIcon = {
|
||||
val icon = it.icon
|
||||
if (it.icon != SearchActionIcon.Custom) {
|
||||
Icon(
|
||||
imageVector = when (it.icon) {
|
||||
SearchActionIcon.Phone -> Icons.Rounded.Call
|
||||
SearchActionIcon.Website -> Icons.Rounded.Language
|
||||
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
|
||||
SearchActionIcon.Timer -> Icons.Rounded.Timer
|
||||
SearchActionIcon.Contact -> Icons.Rounded.Person
|
||||
SearchActionIcon.Email -> Icons.Rounded.Email
|
||||
SearchActionIcon.Message -> Icons.Rounded.Sms
|
||||
SearchActionIcon.Calendar -> Icons.Rounded.Event
|
||||
SearchActionIcon.Translate -> Icons.Rounded.Translate
|
||||
else -> Icons.Rounded.Search
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = if (it.iconColor == 0) MaterialTheme.colorScheme.primary else Color(
|
||||
it.iconColor
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
/*leadingIcon = {
|
||||
val icon = it.icon
|
||||
if (icon == null) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = if (it.color == 0) MaterialTheme.colorScheme.primary else Color(
|
||||
it.color
|
||||
)
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
modifier = Modifier.size(24.dp),
|
||||
model = File(icon),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}*/
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package de.mm20.launcher2.ui.launcher.searchbar
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.HelpOutline
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Wallpaper
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||
|
||||
@Composable
|
||||
fun RowScope.SearchBarMenu(
|
||||
searchBarValue: String,
|
||||
onSearchBarValueChange: (newValue: String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
||||
|
||||
IconButton(onClick = {
|
||||
if (searchBarValue.isNotBlank()) onSearchBarValueChange("")
|
||||
else showOverflowMenu = true
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(
|
||||
rightIcon,
|
||||
atEnd = searchBarValue.isNotEmpty()
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = LocalContentColor.current
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = showOverflowMenu, onDismissRequest = { showOverflowMenu = false }) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_SET_WALLPAPER),
|
||||
null
|
||||
)
|
||||
)
|
||||
showOverflowMenu = false
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.wallpaper))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Wallpaper, contentDescription = null)
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||
showOverflowMenu = false
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.settings))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Settings, contentDescription = null)
|
||||
}
|
||||
)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(colorScheme.primaryContainer.toArgb())
|
||||
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
.launchUrl(context, Uri.parse("https://kvaesitso.mm20.de/docs/user-guide"))
|
||||
showOverflowMenu = false
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.help))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.HelpOutline, contentDescription = null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package de.mm20.launcher2.ui.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.accompanist.navigation.animation.AnimatedNavHost
|
||||
import com.google.accompanist.navigation.animation.composable
|
||||
@@ -16,7 +18,6 @@ import de.mm20.launcher2.licenses.AppLicense
|
||||
import de.mm20.launcher2.licenses.OpenSourceLicenses
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||
import de.mm20.launcher2.ui.base.BaseActivity
|
||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||
@@ -43,11 +44,13 @@ import de.mm20.launcher2.ui.settings.license.LicenseScreen
|
||||
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.websearch.WebSearchSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
|
||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -119,6 +122,9 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/search/websearch") {
|
||||
WebSearchSettingsScreen()
|
||||
}
|
||||
composable("settings/search/searchactions") {
|
||||
SearchActionsSettingsScreen()
|
||||
}
|
||||
composable("settings/search/hiddenitems") {
|
||||
HiddenItemsSettingsScreen()
|
||||
}
|
||||
@@ -164,7 +170,8 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/debug/crashreporter") {
|
||||
CrashReporterScreen()
|
||||
}
|
||||
composable("settings/debug/crashreporter/report?fileName={fileName}",
|
||||
composable(
|
||||
"settings/debug/crashreporter/report?fileName={fileName}",
|
||||
arguments = listOf(navArgument("fileName") {
|
||||
nullable = false
|
||||
})
|
||||
|
||||
+2
-3
@@ -38,11 +38,11 @@ import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
||||
import de.mm20.launcher2.preferences.Settings.SystemBarsSettings.SystemBarColors
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.SearchBar
|
||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||
import de.mm20.launcher2.ui.component.getShape
|
||||
import de.mm20.launcher2.ui.component.preferences.*
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||
import de.mm20.launcher2.ui.theme.getTypography
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -389,7 +389,6 @@ fun SearchBarStylePreference(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
level = level,
|
||||
style = styles[it],
|
||||
websearches = emptyList(),
|
||||
value = previewSearchValue,
|
||||
onValueChange = {})
|
||||
}
|
||||
|
||||
@@ -169,17 +169,12 @@ fun SearchSettingsScreen() {
|
||||
}
|
||||
)
|
||||
|
||||
val webSearch by viewModel.webSearch.observeAsState()
|
||||
PreferenceWithSwitch(
|
||||
title = stringResource(R.string.preference_search_websearch),
|
||||
summary = stringResource(R.string.preference_search_websearch_summary),
|
||||
icon = Icons.Rounded.TravelExplore,
|
||||
switchValue = webSearch == true,
|
||||
onSwitchChanged = {
|
||||
viewModel.setWebSearch(it)
|
||||
},
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_screen_search_actions),
|
||||
summary = stringResource(R.string.preference_search_search_actions_summary),
|
||||
icon = Icons.Rounded.ArrowOutward,
|
||||
onClick = {
|
||||
navController?.navigate("settings/search/websearch")
|
||||
navController?.navigate("settings/search/searchactions")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package de.mm20.launcher2.ui.settings.searchactions
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Alarm
|
||||
import androidx.compose.material.icons.rounded.CalendarToday
|
||||
import androidx.compose.material.icons.rounded.Call
|
||||
import androidx.compose.material.icons.rounded.Email
|
||||
import androidx.compose.material.icons.rounded.Event
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Sms
|
||||
import androidx.compose.material.icons.rounded.Timer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
|
||||
|
||||
@Composable
|
||||
fun SearchActionsSettingsScreen() {
|
||||
val viewModel: SearchActionsSettingsScreenVM = viewModel()
|
||||
val settings by viewModel.searchActionSettings.observeAsState(
|
||||
Settings.SearchActionSettings.getDefaultInstance()
|
||||
)
|
||||
|
||||
PreferenceScreen(stringResource(id = R.string.preference_screen_search_actions)) {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Call,
|
||||
title = stringResource(R.string.search_action_call),
|
||||
value = settings.call,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setCall(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Sms,
|
||||
title = stringResource(R.string.search_action_message),
|
||||
value = settings.message,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setMessage(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Email,
|
||||
title = stringResource(R.string.search_action_email),
|
||||
value = settings.email,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setEmail(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Person,
|
||||
title = stringResource(R.string.search_action_contact),
|
||||
value = settings.contact,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setContact(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Alarm,
|
||||
title = stringResource(R.string.search_action_alarm),
|
||||
value = settings.setAlarm,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setSetAlarm(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Timer,
|
||||
title = stringResource(R.string.search_action_timer),
|
||||
value = settings.startTimer,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setStartTimer(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Event,
|
||||
title = stringResource(R.string.search_action_event),
|
||||
value = settings.scheduleEvent,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setScheduleEvent(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Language,
|
||||
title = stringResource(R.string.search_action_open_url),
|
||||
value = settings.openUrl,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setOpenUrl(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package de.mm20.launcher2.ui.settings.searchactions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
val searchActionSettings = dataStore.data.map { it.searchActions }.asLiveData()
|
||||
|
||||
fun updateSettings(block: SearchActionSettings.Builder.() -> SearchActionSettings.Builder) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchActions(
|
||||
it.searchActions.toBuilder().block()
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user