Searchbar: Launch on enter / done (#258)

* Search: add launch action on keyboard-done

* Add switch in settings

* :)

* fix dataStore-update, pull bestMatch from multiple sources

* launch searchAction when no other match is found

* update localization

* switch to .firstNotNullOfOrNull { }

* switch imeAction based on launchOnEnter == true

* localization

* adding highlighting for bestMatch

* Remove unused code

* Store hightlighted search result in viewmodel

* Add highlight for list item, tweak grid item highlight

* Highlight search action that is launched on enter

* localizaton

* switch to IconShape / hairline border

* remove border / outlineVariant-background

---------

Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
Christoph 2023-02-19 18:51:31 +01:00 committed by GitHub
parent 72b42a1105
commit b11ba9e23a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 298 additions and 405 deletions

View File

@ -21,6 +21,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
@ -199,6 +200,7 @@ fun AssistantScaffold(
viewModel.setSearchbarFocus(it) viewModel.setSearchbarFocus(it)
}, },
actions = actions, actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction,
showHiddenItemsButton = true, showHiddenItemsButton = true,
value = { value }, value = { value },
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },

View File

@ -8,11 +8,9 @@ import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -24,41 +22,56 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.locals.LocalCardStyle
import kotlin.math.ln import kotlin.math.ln
@Composable @Composable
fun InnerCard( fun InnerCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
raised: Boolean = false, raised: Boolean = false,
highlight: Boolean = false,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val transition = updateTransition(raised, label = "InnerCard") val transition = updateTransition(InnerCardStyle(raised, highlight), label = "InnerCard")
val absoluteTonalElevation = LocalAbsoluteTonalElevation.current val absoluteTonalElevation = LocalAbsoluteTonalElevation.current
val elevation by transition.animateDp(label = "elevation", transitionSpec = { val elevation by transition.animateDp(label = "elevation", transitionSpec = {
tween(250, if (targetState) 250 else 0) tween(250, if (targetState == InnerCardStyle.Raised) 250 else 0)
}) { }) {
if(it) 4.dp else 0.dp if (it == InnerCardStyle.Raised) 2.dp else 0.dp
} }
val borderWidth by transition.animateDp(label = "borderWidth", transitionSpec = { tween(500) }) { val borderWidth by transition.animateDp(
if (it) 0.dp else 1.dp label = "borderWidth",
transitionSpec = { tween(500) }) {
when (it) {
InnerCardStyle.Raised -> 0.dp
InnerCardStyle.Highlighted -> 1.dp
InnerCardStyle.Default -> 1.dp
}
} }
val borderColor by transition.animateColor(label = "borderColor", transitionSpec = { tween(500) }) { val borderColor by transition.animateColor(
MaterialTheme.colorScheme.outline.copy(alpha = if (it) 0f else 0.7f) label = "borderColor",
transitionSpec = { tween(500) }) {
when (it) {
InnerCardStyle.Raised -> Color.Transparent
InnerCardStyle.Highlighted -> MaterialTheme.colorScheme.secondary
InnerCardStyle.Default -> MaterialTheme.colorScheme.outlineVariant
}
} }
val bgAlpha by transition.animateFloat(label = "bgAlpha", transitionSpec = {
tween(250, if (targetState) 0 else 250) val bgColor by transition.animateColor(label = "bgColor", transitionSpec = {
tween(250, if (targetState == InnerCardStyle.Raised) 0 else 250)
}) { }) {
if (it) 1f else 0f if (it == InnerCardStyle.Highlighted) {
MaterialTheme.colorScheme.secondaryContainer
} else {
MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation + elevation)
}
} }
val shape = MaterialTheme.shapes.small val shape = MaterialTheme.shapes.small
val bgColor = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation + elevation)
Box( Box(
modifier = modifier modifier = modifier
.border(BorderStroke(borderWidth, borderColor), shape) .border(BorderStroke(borderWidth, borderColor), shape)
@ -66,10 +79,9 @@ fun InnerCard(
.clip(shape) .clip(shape)
.drawBehind { .drawBehind {
drawRect( drawRect(
bgColor.copy(alpha = bgAlpha) bgColor
) )
} },
,
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalAbsoluteTonalElevation provides absoluteTonalElevation LocalAbsoluteTonalElevation provides absoluteTonalElevation
@ -79,6 +91,20 @@ fun InnerCard(
} }
} }
internal enum class InnerCardStyle {
Default,
Highlighted,
Raised,
}
internal fun InnerCardStyle(raised: Boolean, highlight: Boolean): InnerCardStyle {
return when {
raised -> InnerCardStyle.Raised
highlight -> InnerCardStyle.Highlighted
else -> InnerCardStyle.Default
}
}
internal fun ColorScheme.surfaceColorAtElevation( internal fun ColorScheme.surfaceColorAtElevation(
elevation: Dp, elevation: Dp,
): Color { ): Color {

View File

@ -15,6 +15,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -34,6 +37,7 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
@ -54,6 +58,7 @@ fun SearchBar(
darkColors: Boolean = false, darkColors: Boolean = false,
menu: @Composable RowScope.() -> Unit = {}, menu: @Composable RowScope.() -> Unit = {},
actions: @Composable ColumnScope.() -> Unit = {}, actions: @Composable ColumnScope.() -> Unit = {},
onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null
) { ) {
val transition = updateTransition(level, label = "Searchbar") val transition = updateTransition(level, label = "Searchbar")
@ -171,7 +176,17 @@ fun SearchBar(
singleLine = true, singleLine = true,
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary) cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
keyboardOptions = KeyboardOptions(
imeAction = if (onKeyboardActionGo == null) {
ImeAction.Search
} else {
ImeAction.Go
}
),
keyboardActions = KeyboardActions(
onGo = onKeyboardActionGo
)
) )
} }
Row( Row(

View File

@ -228,13 +228,6 @@ fun ShapedLauncherIcon(
} }
} }
@Composable
private fun Badge(
badge: () -> Badge?
) {
}
@Composable @Composable
private fun IconLayer( private fun IconLayer(
layer: LauncherIconLayer, layer: LauncherIconLayer,

View File

@ -56,6 +56,8 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
val statusBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData() val statusBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
val navBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData() val navBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }.asLiveData()
val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }.asLiveData() val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }.asLiveData()
val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }.asLiveData() val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }.asLiveData()

View File

@ -69,6 +69,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.gestures.LocalGestureDetector import de.mm20.launcher2.ui.gestures.LocalGestureDetector
@ -503,6 +504,7 @@ fun PagerScaffold(
viewModel.setSearchbarFocus(it) viewModel.setSearchbarFocus(it)
}, },
actions = actions, actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction,
showHiddenItemsButton = isSearchOpen, showHiddenItemsButton = isSearchOpen,
value = { value }, value = { value },
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },

View File

@ -56,6 +56,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@ -66,6 +67,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.gestures.LocalGestureDetector import de.mm20.launcher2.ui.gestures.LocalGestureDetector
@ -89,6 +91,7 @@ fun PullDownScaffold(
bottomSearchBar: Boolean = false, bottomSearchBar: Boolean = false,
reverseSearchResults: Boolean = false, reverseSearchResults: Boolean = false,
fixedSearchBar: Boolean = false, fixedSearchBar: Boolean = false,
launchOnEnter: Boolean = false
) { ) {
val viewModel: LauncherScaffoldVM = viewModel() val viewModel: LauncherScaffoldVM = viewModel()
val searchVM: SearchVM = viewModel() val searchVM: SearchVM = viewModel()
@ -394,9 +397,13 @@ fun PullDownScaffold(
} }
.pointerInput(gestureManager.shouldDetectDoubleTaps) { .pointerInput(gestureManager.shouldDetectDoubleTaps) {
detectTapGestures( detectTapGestures(
onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) {{ onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) {
if (!isWidgetEditMode) gestureManager.dispatchDoubleTap(it) {
}} else null, if (!isWidgetEditMode) gestureManager.dispatchDoubleTap(
it
)
}
} else null,
onLongPress = { onLongPress = {
if (!isWidgetEditMode) gestureManager.dispatchLongPress( if (!isWidgetEditMode) gestureManager.dispatchLongPress(
it it
@ -518,6 +525,8 @@ fun PullDownScaffold(
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto) val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent) val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
val context = LocalContext.current
LauncherSearchBar( LauncherSearchBar(
modifier = Modifier modifier = Modifier
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter) .align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter)
@ -547,12 +556,16 @@ fun PullDownScaffold(
viewModel.setSearchbarFocus(it) viewModel.setSearchbarFocus(it)
}, },
actions = actions, actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction,
showHiddenItemsButton = isSearchOpen, showHiddenItemsButton = isSearchOpen,
value = { value }, value = { value },
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
style = searchBarStyle, style = searchBarStyle,
reverse = bottomSearchBar, reverse = bottomSearchBar,
onKeyboardActionGo = if (launchOnEnter) {
{ searchVM.launchBestMatchOrAction(context) }
} else null
) )
} }

View File

@ -194,6 +194,7 @@ abstract class SharedLauncherActivity(
when (layout) { when (layout) {
Settings.LayoutSettings.Layout.PullDown -> { Settings.LayoutSettings.Layout.PullDown -> {
key(bottomSearchBar, reverseSearchResults) { key(bottomSearchBar, reverseSearchResults) {
val launchOnEnter by viewModel.launchOnEnter.observeAsState(false)
PullDownScaffold( PullDownScaffold(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -209,6 +210,7 @@ abstract class SharedLauncherActivity(
bottomSearchBar = bottomSearchBar, bottomSearchBar = bottomSearchBar,
reverseSearchResults = reverseSearchResults, reverseSearchResults = reverseSearchResults,
fixedSearchBar = fixedSearchBar, fixedSearchBar = fixedSearchBar,
launchOnEnter = launchOnEnter
) )
} }
} }

View File

@ -94,6 +94,8 @@ fun SearchColumn(
val website by viewModel.websiteResults.observeAsState(emptyList()) val website by viewModel.websiteResults.observeAsState(emptyList())
val hiddenResults by viewModel.hiddenResults.observeAsState(emptyList()) val hiddenResults by viewModel.hiddenResults.observeAsState(emptyList())
val bestMatch by viewModel.bestMatch
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true) val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
val missingCalendarPermission by viewModel.missingCalendarPermission.collectAsState(false) val missingCalendarPermission by viewModel.missingCalendarPermission.collectAsState(false)
@ -191,7 +193,8 @@ fun SearchColumn(
} }
} }
} }
} },
highlightedItem = bestMatch as? SavableSearchable
) )
} }
GridResults( GridResults(
@ -241,7 +244,8 @@ fun SearchColumn(
) )
} }
} }
} else null } else null,
highlightedItem = bestMatch as? SavableSearchable
) )
ListResults( ListResults(
before = if (missingShortcutsPermission && !isSearchEmpty) { before = if (missingShortcutsPermission && !isSearchEmpty) {
@ -267,7 +271,8 @@ fun SearchColumn(
} else null, } else null,
items = appShortcuts.toImmutableList(), items = appShortcuts.toImmutableList(),
reverse = reverse, reverse = reverse,
key = "shortcuts" key = "shortcuts",
highlightedItem = bestMatch as? SavableSearchable
) )
for (conv in unitConverter) { for (conv in unitConverter) {
SingleResult { SingleResult {
@ -300,7 +305,8 @@ fun SearchColumn(
} else null, } else null,
items = events.toImmutableList(), items = events.toImmutableList(),
reverse = reverse, reverse = reverse,
key = "events" key = "events",
highlightedItem = bestMatch as? SavableSearchable
) )
ListResults( ListResults(
before = if (missingContactsPermission && !isSearchEmpty) { before = if (missingContactsPermission && !isSearchEmpty) {
@ -323,7 +329,8 @@ fun SearchColumn(
} else null, } else null,
items = contacts.toImmutableList(), items = contacts.toImmutableList(),
reverse = reverse, reverse = reverse,
key = "contacts" key = "contacts",
highlightedItem = bestMatch as? SavableSearchable
) )
for (wiki in wikipedia) { for (wiki in wikipedia) {
SingleResult { SingleResult {
@ -356,7 +363,8 @@ fun SearchColumn(
} else null, } else null,
items = files.toImmutableList(), items = files.toImmutableList(),
reverse = reverse, reverse = reverse,
key = "files" key = "files",
highlightedItem = bestMatch as? SavableSearchable
) )
} }
@ -373,6 +381,7 @@ fun LazyListScope.GridResults(
key: String, key: String,
before: (@Composable () -> Unit)? = null, before: (@Composable () -> Unit)? = null,
after: (@Composable () -> Unit)? = null, after: (@Composable () -> Unit)? = null,
highlightedItem: SavableSearchable?
) { ) {
if (items.isEmpty() && before == null && after == null) return if (items.isEmpty() && before == null && after == null) return
@ -410,6 +419,7 @@ fun LazyListScope.GridResults(
(it * columns + columns).coerceAtMost(items.size) (it * columns + columns).coerceAtMost(items.size)
), ),
columns = columns, columns = columns,
highlightedItem = highlightedItem
) )
} }
} }
@ -433,6 +443,7 @@ fun GridRow(
items: ImmutableList<SavableSearchable>, items: ImmutableList<SavableSearchable>,
columns: Int, columns: Int,
showLabels: Boolean = LocalGridSettings.current.showLabels, showLabels: Boolean = LocalGridSettings.current.showLabels,
highlightedItem: SavableSearchable?
) { ) {
Row( Row(
@ -442,9 +453,10 @@ fun GridRow(
GridItem( GridItem(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(4.dp, 8.dp), .padding(4.dp),
item = item, item = item,
showLabels = showLabels showLabels = showLabels,
highlight = item.key == highlightedItem?.key
) )
} }
for (i in 0 until columns - items.size) { for (i in 0 until columns - items.size) {
@ -459,6 +471,7 @@ fun LazyListScope.ListResults(
key: String, key: String,
before: (@Composable () -> Unit)? = null, before: (@Composable () -> Unit)? = null,
after: (@Composable () -> Unit)? = null, after: (@Composable () -> Unit)? = null,
highlightedItem: SavableSearchable?
) { ) {
if (before != null) { if (before != null) {
item(key = "$key-before") { item(key = "$key-before") {
@ -488,6 +501,7 @@ fun LazyListScope.ListResults(
bottom = if (if (reverse) it == 0 else it == items.size - 1) 8.dp else 4.dp, bottom = if (if (reverse) it == 0 else it == items.size - 1) 8.dp else 4.dp,
), ),
item = items[it], item = items[it],
highlight = items[it].key == highlightedItem?.key
) )
} }
} }
@ -508,6 +522,7 @@ fun LazyListScope.ListResults(
fun ListRow( fun ListRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
item: SavableSearchable, item: SavableSearchable,
highlight: Boolean
) { ) {
Box( Box(
modifier = modifier.padding( modifier = modifier.padding(
@ -518,7 +533,8 @@ fun ListRow(
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
item = item item = item,
highlight = highlight
) )
} }
} }

View File

@ -1,8 +1,14 @@
package de.mm20.launcher2.ui.launcher.search package de.mm20.launcher2.ui.launcher.search
import android.content.Context
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
@ -10,6 +16,7 @@ import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.search.data.CalendarEvent import de.mm20.launcher2.search.data.CalendarEvent
@ -22,12 +29,18 @@ import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -38,10 +51,12 @@ class SearchVM : ViewModel(), KoinComponent {
private val permissionsManager: PermissionsManager by inject() private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
private val searchService: SearchService by inject() private val searchService: SearchService by inject()
val isSearching = MutableLiveData(false) val searchQuery = MutableLiveData("")
val searchQuery = MutableLiveData<String>("")
val isSearchEmpty = MutableLiveData(true) val isSearchEmpty = MutableLiveData(true)
val appResults = MutableLiveData<List<LauncherApp>>(emptyList()) val appResults = MutableLiveData<List<LauncherApp>>(emptyList())
@ -63,18 +78,32 @@ class SearchVM : ViewModel(), KoinComponent {
private val hiddenItemKeys = favoritesRepository private val hiddenItemKeys = favoritesRepository
.getHiddenItemKeys() .getHiddenItemKeys()
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1) .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val bestMatch = mutableStateOf<Searchable?>(null)
init { init {
search("", true) search("", true)
} }
var searchJob: Job? = null fun launchBestMatchOrAction(context: Context) {
val bestMatch = bestMatch.value
if (bestMatch is SavableSearchable) {
bestMatch.launch(context, null)
return
} else if (bestMatch is SearchAction) {
bestMatch.start(context)
return
}
}
private var searchJob: Job? = null
fun search(query: String, forceRestart: Boolean = false) { fun search(query: String, forceRestart: Boolean = false) {
if (searchQuery.value == query && !forceRestart) return if (searchQuery.value == query && !forceRestart) return
searchQuery.value = query searchQuery.value = query
isSearchEmpty.value = query.isEmpty() isSearchEmpty.value = query.isEmpty()
hiddenResults.value = emptyList() hiddenResults.value = emptyList()
bestMatch.value = null
try { try {
searchJob?.cancel() searchJob?.cancel()
@ -82,7 +111,6 @@ class SearchVM : ViewModel(), KoinComponent {
} }
hideFavorites.postValue(query.isNotEmpty()) hideFavorites.postValue(query.isNotEmpty())
searchJob = viewModelScope.launch { searchJob = viewModelScope.launch {
isSearching.postValue(true)
dataStore.data.collectLatest { dataStore.data.collectLatest {
searchService.search( searchService.search(
@ -128,6 +156,20 @@ class SearchVM : ViewModel(), KoinComponent {
r is SearchAction -> actions.add(r) r is SearchAction -> actions.add(r)
} }
} }
if (query.isNotEmpty() && launchOnEnter.value) {
bestMatch.value = listOf(
apps,
workApps,
shortcuts,
files,
contacts,
events,
wikipedia,
website,
actions
).firstNotNullOfOrNull { it.firstOrNull() }
}
searchActionResults.value = actions searchActionResults.value = actions
appResults.value = apps appResults.value = apps
workAppResults.value = workApps workAppResults.value = workApps
@ -143,9 +185,6 @@ class SearchVM : ViewModel(), KoinComponent {
} }
} }
} }
isSearching.postValue(false)
} }
} }
@ -168,7 +207,6 @@ class SearchVM : ViewModel(), KoinComponent {
} }
} }
val missingContactsPermission = combine( val missingContactsPermission = combine(
permissionsManager.hasPermission(PermissionGroup.Contacts), permissionsManager.hasPermission(PermissionGroup.Contacts),
dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged() dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged()

View File

@ -1,30 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.apps
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
@Composable
fun AppResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val apps by viewModel.appResults.observeAsState(emptyList())
if (apps.isNotEmpty()) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
) {
SearchResultGrid(items = apps, reverse = reverse)
}
}
}

View File

@ -1,69 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.appshortcuts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
@Composable
fun ColumnScope.AppShortcutResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val shortcuts by viewModel.appShortcutResults.observeAsState(emptyList())
val context = LocalContext.current
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
val missingPermission by viewModel.missingAppShortcutPermission.collectAsState(false)
AnimatedVisibility(shortcuts.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
) {
Column {
AnimatedVisibility(!isSearchEmpty && missingPermission) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_appshortcuts_search, stringResource(R.string.app_name)),
onClick = { viewModel.requestAppShortcutPermission(context as AppCompatActivity) },
modifier = Modifier.padding(16.dp),
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableAppShortcutSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
}
SearchResultList(
items = shortcuts,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
reverse = reverse
)
}
}
}

View File

@ -1,69 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.calendar
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
@Composable
fun ColumnScope.CalendarResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val calendarEvents by viewModel.calendarResults.observeAsState(emptyList())
val context = LocalContext.current
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
val missingPermission by viewModel.missingCalendarPermission.collectAsState(false)
AnimatedVisibility(calendarEvents.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
.fillMaxWidth()
) {
Column {
AnimatedVisibility(!isSearchEmpty && missingPermission) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search),
onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) },
modifier = Modifier.padding(16.dp),
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableCalendarSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
SearchResultList(
items = calendarEvents,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
reverse = reverse
)
}
}
}
}

View File

@ -4,13 +4,28 @@ import android.content.ComponentName
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.* import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
@ -18,14 +33,22 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.* import de.mm20.launcher2.search.data.AppShortcut
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.Website
import de.mm20.launcher2.search.data.Wikipedia
import de.mm20.launcher2.ui.component.LauncherCard import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.LocalIconShape
import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.toDp import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.ktx.toPixels
@ -45,13 +68,19 @@ import kotlinx.coroutines.delay
@Composable @Composable
fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels: Boolean = true) { fun GridItem(
modifier: Modifier = Modifier,
item: SavableSearchable,
showLabels: Boolean = true,
highlight: Boolean = false
) {
val viewModel = remember(item.key) { GridItemVM(item) } val viewModel = remember(item.key) { GridItemVM(item) }
val context = LocalContext.current val context = LocalContext.current
var showPopup by remember(item.key) { mutableStateOf(false) } var showPopup by remember(item.key) { mutableStateOf(false) }
var bounds by remember { mutableStateOf(Rect.Zero) } var bounds by remember { mutableStateOf(Rect.Zero) }
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
val badge by remember(item.key) { viewModel.badge }.collectAsState(null) val badge by remember(item.key) { viewModel.badge }.collectAsState(null)
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels() val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
@ -72,7 +101,9 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels:
return@HandleHomeTransition HomeTransitionParams( return@HandleHomeTransition HomeTransitionParams(
bounds bounds
) { _, _ -> ) { _, _ ->
ShapedLauncherIcon(size = LocalGridSettings.current.iconSize.dp, icon = { icon }) ShapedLauncherIcon(
size = LocalGridSettings.current.iconSize.dp,
icon = { icon })
} }
} }
return@HandleHomeTransition null return@HandleHomeTransition null
@ -80,29 +111,42 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels:
} }
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
ShapedLauncherIcon( val iconShape = LocalIconShape.current
modifier = Modifier
.onGloballyPositioned { Box(
bounds = it.boundsInWindow() modifier = if (highlight) {
Modifier
.background(
MaterialTheme.colorScheme.outlineVariant,
iconShape
)
} else Modifier,
) {
ShapedLauncherIcon(
modifier = Modifier
.padding(4.dp)
.onGloballyPositioned {
bounds = it.boundsInWindow()
},
size = LocalGridSettings.current.iconSize.dp,
badge = { badge },
icon = { icon },
onClick = {
if (!launchOnPress || !viewModel.launch(context, bounds)) {
showPopup = true
}
}, },
size = LocalGridSettings.current.iconSize.dp, onLongClick = {
badge = { badge }, hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
icon = { icon },
onClick = {
if (!launchOnPress || !viewModel.launch(context, bounds)) {
showPopup = true showPopup = true
} }
}, )
onLongClick = { }
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
showPopup = true
}
)
if (showLabels) { if (showLabels) {
Text( Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp), .padding(vertical = 4.dp),
text = item.labelOverride ?: item.label, text = item.labelOverride ?: item.label,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@ -110,6 +154,7 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels:
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
if (showPopup) { if (showPopup) {
ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false }) ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false })
} }
@ -158,6 +203,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
x = ((1 - animationProgress) * origin.left).toDp() - 16.dp * (1 - animationProgress), x = ((1 - animationProgress) * origin.left).toDp() - 16.dp * (1 - animationProgress),
) )
.wrapContentSize() .wrapContentSize()
.padding(4.dp)
) { ) {
when (searchable) { when (searchable) {
is LauncherApp -> { is LauncherApp -> {
@ -171,6 +217,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
} }
) )
} }
is Website -> { is Website -> {
WebsiteItemGridPopup( WebsiteItemGridPopup(
website = searchable, website = searchable,
@ -182,6 +229,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
} }
) )
} }
is Wikipedia -> { is Wikipedia -> {
WikipediaItemGridPopup( WikipediaItemGridPopup(
wikipedia = searchable, wikipedia = searchable,
@ -193,6 +241,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
} }
) )
} }
is Contact -> { is Contact -> {
ContactItemGridPopup( ContactItemGridPopup(
contact = searchable, contact = searchable,
@ -204,6 +253,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
} }
) )
} }
is File -> { is File -> {
FileItemGridPopup( FileItemGridPopup(
file = searchable, file = searchable,
@ -215,6 +265,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
} }
) )
} }
is CalendarEvent -> { is CalendarEvent -> {
CalendarItemGridPopup( CalendarItemGridPopup(
calendar = searchable, calendar = searchable,
@ -226,6 +277,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
} }
) )
} }
is AppShortcut -> { is AppShortcut -> {
ShortcutItemGridPopup( ShortcutItemGridPopup(
shortcut = searchable, shortcut = searchable,

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.search.common.grid
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -16,7 +17,8 @@ fun SearchResultGrid(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showLabels: Boolean = LocalGridSettings.current.showLabels, showLabels: Boolean = LocalGridSettings.current.showLabels,
columns: Int = LocalGridSettings.current.columnCount, columns: Int = LocalGridSettings.current.columnCount,
reverse: Boolean = false reverse: Boolean = false,
highlightedItem: SavableSearchable? = null
) { ) {
Column( Column(
modifier = modifier modifier = modifier
@ -33,9 +35,10 @@ fun SearchResultGrid(
GridItem( GridItem(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(4.dp, 8.dp), .padding(4.dp),
item = item, item = item,
showLabels = showLabels showLabels = showLabels,
highlight = item.key == highlightedItem?.key
) )
} else { } else {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))

View File

@ -17,7 +17,11 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItem
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
@Composable @Composable
fun ListItem(modifier: Modifier = Modifier, item: SavableSearchable) { fun ListItem(
modifier: Modifier = Modifier,
item: SavableSearchable,
highlight: Boolean = false
) {
var showDetails by remember { mutableStateOf(false) } var showDetails by remember { mutableStateOf(false) }
val context = LocalContext.current val context = LocalContext.current
@ -29,6 +33,7 @@ fun ListItem(modifier: Modifier = Modifier, item: SavableSearchable) {
.onGloballyPositioned { .onGloballyPositioned {
bounds = it.boundsInWindow() bounds = it.boundsInWindow()
}, },
highlight = highlight,
raised = showDetails raised = showDetails
) { ) {
when (item) { when (item) {

View File

@ -15,7 +15,8 @@ import de.mm20.launcher2.ui.layout.BottomReversed
fun SearchResultList( fun SearchResultList(
items: List<SavableSearchable>, items: List<SavableSearchable>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
reverse: Boolean = false reverse: Boolean = false,
highlightedItem: SavableSearchable? = null
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
@ -27,7 +28,8 @@ fun SearchResultList(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(4.dp), .padding(4.dp),
item = item item = item,
highlight = item.key == highlightedItem?.key
) )
} }
} }

View File

@ -1,68 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.contacts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
@Composable
fun ColumnScope.ContactResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val context = LocalContext.current
val contacts by viewModel.contactResults.observeAsState(emptyList())
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
val missingPermission by viewModel.missingContactsPermission.collectAsState(false)
AnimatedVisibility(contacts.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
.fillMaxWidth()
) {
Column {
AnimatedVisibility(!isSearchEmpty && missingPermission) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_contact_search),
onClick = { viewModel.requestContactsPermission(context as AppCompatActivity) },
modifier = Modifier.padding(16.dp),
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableContactsSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
SearchResultList(
items = contacts,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
reverse = reverse
)
}
}
}
}

View File

@ -1,67 +0,0 @@
package de.mm20.launcher2.ui.launcher.search.files
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.search.common.list.SearchResultList
@Composable
fun ColumnScope.FileResults(reverse: Boolean = false) {
val viewModel: SearchVM = viewModel()
val files by viewModel.fileResults.observeAsState(emptyList())
val context = LocalContext.current
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
val missingPermission by viewModel.missingFilesPermission.collectAsState(false)
AnimatedVisibility(files.isNotEmpty() || (!isSearchEmpty && missingPermission)) {
LauncherCard(
modifier = Modifier
.padding(bottom = if (reverse) 0.dp else 8.dp, top = if (reverse) 8.dp else 0.dp)
.fillMaxWidth()
) {
Column {
AnimatedVisibility(!isSearchEmpty && missingPermission) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_files_search),
onClick = { viewModel.requestFilesPermission(context as AppCompatActivity) },
modifier = Modifier.padding(16.dp),
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableFilesSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
SearchResultList(
items = files,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
reverse = reverse
)}
}
}
}

View File

@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilledIconButton
@ -13,9 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@ -24,7 +23,6 @@ import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.ui.component.SearchBar import de.mm20.launcher2.ui.component.SearchBar
import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
@ -38,9 +36,11 @@ fun LauncherSearchBar(
focused: Boolean, focused: Boolean,
onFocusChange: (Boolean) -> Unit, onFocusChange: (Boolean) -> Unit,
actions: List<SearchAction>, actions: List<SearchAction>,
highlightedAction: SearchAction?,
showHiddenItemsButton: Boolean = false, showHiddenItemsButton: Boolean = false,
reverse: Boolean = false, reverse: Boolean = false,
darkColors: Boolean = false, darkColors: Boolean = false,
onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -51,7 +51,6 @@ fun LauncherSearchBar(
val hiddenItems by searchVM.hiddenResults.observeAsState(emptyList()) val hiddenItems by searchVM.hiddenResults.observeAsState(emptyList())
LaunchedEffect(focused) { LaunchedEffect(focused) {
if (focused) focusRequester.requestFocus() if (focused) focusRequester.requestFocus()
else focusManager.clearFocus() else focusManager.clearFocus()
@ -80,10 +79,11 @@ fun LauncherSearchBar(
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange) SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
}, },
actions = { actions = {
SearchBarActions(actions = actions, reverse = reverse) SearchBarActions(actions = actions, reverse = reverse, highlightedAction = highlightedAction)
}, },
focusRequester = focusRequester, focusRequester = focusRequester,
onFocus = { onFocusChange(true) }, onFocus = { onFocusChange(true) },
onUnfocus = { onFocusChange(false) }, onUnfocus = { onFocusChange(false) },
onKeyboardActionGo = onKeyboardActionGo
) )
} }

View File

@ -11,8 +11,10 @@ import androidx.compose.foundation.lazy.items
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.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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
@ -28,6 +30,7 @@ import de.mm20.launcher2.ui.settings.SettingsActivity
fun ColumnScope.SearchBarActions( fun ColumnScope.SearchBarActions(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
actions: List<SearchAction>, actions: List<SearchAction>,
highlightedAction: SearchAction?,
reverse: Boolean = false, reverse: Boolean = false,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -42,6 +45,16 @@ fun ColumnScope.SearchBarActions(
items(actions) { items(actions) {
AssistChip( AssistChip(
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
colors = if (it == highlightedAction) {
AssistChipDefaults.assistChipColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
)
} else AssistChipDefaults.assistChipColors(),
border = if (it == highlightedAction) {
AssistChipDefaults.assistChipBorder(
borderColor = MaterialTheme.colorScheme.secondary,
)
} else AssistChipDefaults.assistChipBorder(),
onClick = { onClick = {
it.start(context) it.start(context)
}, },
@ -51,24 +64,6 @@ fun ColumnScope.SearchBarActions(
action = it action = it
) )
} }
/*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
)
}
}*/
) )
} }
item { item {

View File

@ -453,7 +453,8 @@ fun SearchBarStylePreference(
level = level, level = level,
style = styles[it], style = styles[it],
value = previewSearchValue, value = previewSearchValue,
onValueChange = {}) onValueChange = {}
)
} }
HorizontalPagerIndicator(pagerState = pagerState) HorizontalPagerIndicator(pagerState = pagerState)
} }

View File

@ -181,14 +181,24 @@ fun SearchSettingsScreen() {
} }
item { item {
val autoFocus by viewModel.autoFocus.observeAsState() val autoFocus by viewModel.autoFocus.observeAsState()
val launchOnEnter by viewModel.launchOnEnter.observeAsState()
PreferenceCategory { PreferenceCategory {
SwitchPreference( SwitchPreference(
title = stringResource(R.string.preference_search_bar_auto_focus), title = stringResource(R.string.preference_search_bar_auto_focus),
summary = stringResource(R.string.preference_search_bar_auto_focus_summary), summary = stringResource(R.string.preference_search_bar_auto_focus_summary),
value = autoFocus == true, value = autoFocus == true,
onValueChanged = { onValueChanged = {
viewModel.setAutoFocus(it) viewModel.setAutoFocus(it)
}) }
)
SwitchPreference(
title = stringResource(R.string.preference_search_bar_launch_on_enter),
summary = stringResource(R.string.preference_search_bar_launch_on_enter_summary),
value = launchOnEnter == true,
onValueChanged = {
viewModel.setLaunchOnEnter(it)
}
)
Preference( Preference(
title = stringResource(R.string.preference_hidden_items), title = stringResource(R.string.preference_hidden_items),
summary = stringResource(R.string.preference_hidden_items_summary), summary = stringResource(R.string.preference_hidden_items_summary),

View File

@ -151,6 +151,20 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
} }
} }
val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }.asLiveData()
fun setLaunchOnEnter(launchOnEnter: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSearchBar(
it.searchBar.toBuilder()
.setLaunchOnEnter(launchOnEnter)
)
.build()
}
}
}
val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData() val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData()
val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData() val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData()

View File

@ -347,6 +347,8 @@
<string name="preference_search_bar_style_summary">Erscheinungsbild der Suchleiste anpassen</string> <string name="preference_search_bar_style_summary">Erscheinungsbild der Suchleiste anpassen</string>
<string name="preference_search_bar_auto_focus">Tastatur öffnen</string> <string name="preference_search_bar_auto_focus">Tastatur öffnen</string>
<string name="preference_search_bar_auto_focus_summary">Bildschirmtastatur beim Öffnen der Suche automatisch einblenden</string> <string name="preference_search_bar_auto_focus_summary">Bildschirmtastatur beim Öffnen der Suche automatisch einblenden</string>
<string name="preference_search_bar_launch_on_enter">Eingabe zum Starten</string>
<string name="preference_search_bar_launch_on_enter_summary">Hervorgehobenes Suchergebnis oder Schnellaktion beim Berühren der Start-Taste aufrufen</string>
<string name="preference_wikipedia_customurl">Wikipedia-URL</string> <string name="preference_wikipedia_customurl">Wikipedia-URL</string>
<string name="music_widget_default_title">%1$s spielt Medien</string> <string name="music_widget_default_title">%1$s spielt Medien</string>
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string> <string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>

View File

@ -631,6 +631,8 @@
<string name="preference_search_bar_color">Color</string> <string name="preference_search_bar_color">Color</string>
<string name="preference_search_bar_auto_focus">Open keyboard</string> <string name="preference_search_bar_auto_focus">Open keyboard</string>
<string name="preference_search_bar_auto_focus_summary">Automatically show the keyboard when opening the search</string> <string name="preference_search_bar_auto_focus_summary">Automatically show the keyboard when opening the search</string>
<string name="preference_search_bar_launch_on_enter">Launch on enter</string>
<string name="preference_search_bar_launch_on_enter_summary">Launch highlighted match or quick-action upon tapping go</string>
<string name="preference_hidden_items">Hidden search results</string> <string name="preference_hidden_items">Hidden search results</string>
<string name="preference_hidden_items_summary">Manage hidden apps and search results</string> <string name="preference_hidden_items_summary">Manage hidden apps and search results</string>
<string name="preference_screen_tags">Tags</string> <string name="preference_screen_tags">Tags</string>

View File

@ -222,6 +222,7 @@ message Settings {
Dark = 2; Dark = 2;
} }
SearchBarColors color = 3; SearchBarColors color = 3;
bool launch_on_enter = 4;
} }
SearchBarSettings search_bar = 20; SearchBarSettings search_bar = 20;