diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt index a7ab9a01..15bc47e9 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt @@ -21,6 +21,7 @@ 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.searchactions.actions.SearchAction import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur @@ -199,6 +200,7 @@ fun AssistantScaffold( viewModel.setSearchbarFocus(it) }, actions = actions, + highlightedAction = searchVM.bestMatch.value as? SearchAction, showHiddenItemsButton = true, value = { value }, onValueChange = { searchVM.search(it) }, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/InnerCard.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/InnerCard.kt index fa725a78..fe52899a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/InnerCard.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/InnerCard.kt @@ -8,11 +8,9 @@ import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ColorScheme import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider 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.unit.Dp import androidx.compose.ui.unit.dp -import de.mm20.launcher2.ui.locals.LocalCardStyle import kotlin.math.ln @Composable fun InnerCard( modifier: Modifier = Modifier, raised: Boolean = false, + highlight: Boolean = false, content: @Composable () -> Unit ) { - val transition = updateTransition(raised, label = "InnerCard") + val transition = updateTransition(InnerCardStyle(raised, highlight), label = "InnerCard") val absoluteTonalElevation = LocalAbsoluteTonalElevation.current 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) }) { - if (it) 0.dp else 1.dp + val borderWidth by transition.animateDp( + 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) }) { - MaterialTheme.colorScheme.outline.copy(alpha = if (it) 0f else 0.7f) + val borderColor by transition.animateColor( + 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 bgColor = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation + elevation) - Box( modifier = modifier .border(BorderStroke(borderWidth, borderColor), shape) @@ -66,10 +79,9 @@ fun InnerCard( .clip(shape) .drawBehind { drawRect( - bgColor.copy(alpha = bgAlpha) + bgColor ) - } - , + }, ) { CompositionLocalProvider( 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( elevation: Dp, ): Color { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt index 0e7603cf..518cb28f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt @@ -15,6 +15,9 @@ 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.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.material3.Icon 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.SolidColor import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.ui.R @@ -54,6 +58,7 @@ fun SearchBar( darkColors: Boolean = false, menu: @Composable RowScope.() -> Unit = {}, actions: @Composable ColumnScope.() -> Unit = {}, + onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null ) { val transition = updateTransition(level, label = "Searchbar") @@ -171,7 +176,17 @@ fun SearchBar( singleLine = true, value = value, 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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt index dff359cd..648a2413 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/ShapedLauncherIcon.kt @@ -228,13 +228,6 @@ fun ShapedLauncherIcon( } } -@Composable -private fun Badge( - badge: () -> Badge? -) { - -} - @Composable private fun IconLayer( layer: LauncherIconLayer, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt index 78571a76..4150d4c9 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/LauncherScaffoldVM.kt @@ -56,6 +56,8 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent { val statusBarColor = 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 hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }.asLiveData() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt index 175bbade..71851a6e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt @@ -69,6 +69,7 @@ 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.searchactions.actions.SearchAction import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.gestures.LocalGestureDetector @@ -503,6 +504,7 @@ fun PagerScaffold( viewModel.setSearchbarFocus(it) }, actions = actions, + highlightedAction = searchVM.bestMatch.value as? SearchAction, showHiddenItemsButton = isSearchOpen, value = { value }, onValueChange = { searchVM.search(it) }, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt index 7da31bec..1064cfa3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -66,6 +67,7 @@ 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.searchactions.actions.SearchAction import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.SearchBarLevel import de.mm20.launcher2.ui.gestures.LocalGestureDetector @@ -89,6 +91,7 @@ fun PullDownScaffold( bottomSearchBar: Boolean = false, reverseSearchResults: Boolean = false, fixedSearchBar: Boolean = false, + launchOnEnter: Boolean = false ) { val viewModel: LauncherScaffoldVM = viewModel() val searchVM: SearchVM = viewModel() @@ -394,9 +397,13 @@ fun PullDownScaffold( } .pointerInput(gestureManager.shouldDetectDoubleTaps) { detectTapGestures( - onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) {{ - if (!isWidgetEditMode) gestureManager.dispatchDoubleTap(it) - }} else null, + onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) { + { + if (!isWidgetEditMode) gestureManager.dispatchDoubleTap( + it + ) + } + } else null, onLongPress = { if (!isWidgetEditMode) gestureManager.dispatchLongPress( it @@ -518,6 +525,8 @@ fun PullDownScaffold( val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto) val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent) + val context = LocalContext.current + LauncherSearchBar( modifier = Modifier .align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter) @@ -547,12 +556,16 @@ fun PullDownScaffold( viewModel.setSearchbarFocus(it) }, actions = actions, + highlightedAction = searchVM.bestMatch.value as? SearchAction, showHiddenItemsButton = isSearchOpen, value = { value }, onValueChange = { searchVM.search(it) }, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark, style = searchBarStyle, reverse = bottomSearchBar, + onKeyboardActionGo = if (launchOnEnter) { + { searchVM.launchBestMatchOrAction(context) } + } else null ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt index 9581e395..5b1e2ae1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/SharedLauncherActivity.kt @@ -194,6 +194,7 @@ abstract class SharedLauncherActivity( when (layout) { Settings.LayoutSettings.Layout.PullDown -> { key(bottomSearchBar, reverseSearchResults) { + val launchOnEnter by viewModel.launchOnEnter.observeAsState(false) PullDownScaffold( modifier = Modifier .fillMaxSize() @@ -209,6 +210,7 @@ abstract class SharedLauncherActivity( bottomSearchBar = bottomSearchBar, reverseSearchResults = reverseSearchResults, fixedSearchBar = fixedSearchBar, + launchOnEnter = launchOnEnter ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index 2df8edbd..60a8da4b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -94,6 +94,8 @@ fun SearchColumn( val website by viewModel.websiteResults.observeAsState(emptyList()) val hiddenResults by viewModel.hiddenResults.observeAsState(emptyList()) + val bestMatch by viewModel.bestMatch + val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true) val missingCalendarPermission by viewModel.missingCalendarPermission.collectAsState(false) @@ -191,7 +193,8 @@ fun SearchColumn( } } } - } + }, + highlightedItem = bestMatch as? SavableSearchable ) } GridResults( @@ -241,7 +244,8 @@ fun SearchColumn( ) } } - } else null + } else null, + highlightedItem = bestMatch as? SavableSearchable ) ListResults( before = if (missingShortcutsPermission && !isSearchEmpty) { @@ -267,7 +271,8 @@ fun SearchColumn( } else null, items = appShortcuts.toImmutableList(), reverse = reverse, - key = "shortcuts" + key = "shortcuts", + highlightedItem = bestMatch as? SavableSearchable ) for (conv in unitConverter) { SingleResult { @@ -300,7 +305,8 @@ fun SearchColumn( } else null, items = events.toImmutableList(), reverse = reverse, - key = "events" + key = "events", + highlightedItem = bestMatch as? SavableSearchable ) ListResults( before = if (missingContactsPermission && !isSearchEmpty) { @@ -323,7 +329,8 @@ fun SearchColumn( } else null, items = contacts.toImmutableList(), reverse = reverse, - key = "contacts" + key = "contacts", + highlightedItem = bestMatch as? SavableSearchable ) for (wiki in wikipedia) { SingleResult { @@ -356,7 +363,8 @@ fun SearchColumn( } else null, items = files.toImmutableList(), reverse = reverse, - key = "files" + key = "files", + highlightedItem = bestMatch as? SavableSearchable ) } @@ -373,6 +381,7 @@ fun LazyListScope.GridResults( key: String, before: (@Composable () -> Unit)? = null, after: (@Composable () -> Unit)? = null, + highlightedItem: SavableSearchable? ) { if (items.isEmpty() && before == null && after == null) return @@ -410,6 +419,7 @@ fun LazyListScope.GridResults( (it * columns + columns).coerceAtMost(items.size) ), columns = columns, + highlightedItem = highlightedItem ) } } @@ -433,6 +443,7 @@ fun GridRow( items: ImmutableList, columns: Int, showLabels: Boolean = LocalGridSettings.current.showLabels, + highlightedItem: SavableSearchable? ) { Row( @@ -442,9 +453,10 @@ fun GridRow( GridItem( modifier = Modifier .weight(1f) - .padding(4.dp, 8.dp), + .padding(4.dp), item = item, - showLabels = showLabels + showLabels = showLabels, + highlight = item.key == highlightedItem?.key ) } for (i in 0 until columns - items.size) { @@ -459,6 +471,7 @@ fun LazyListScope.ListResults( key: String, before: (@Composable () -> Unit)? = null, after: (@Composable () -> Unit)? = null, + highlightedItem: SavableSearchable? ) { if (before != null) { 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, ), item = items[it], + highlight = items[it].key == highlightedItem?.key ) } } @@ -508,6 +522,7 @@ fun LazyListScope.ListResults( fun ListRow( modifier: Modifier = Modifier, item: SavableSearchable, + highlight: Boolean ) { Box( modifier = modifier.padding( @@ -518,7 +533,8 @@ fun ListRow( ListItem( modifier = Modifier .fillMaxWidth(), - item = item + item = item, + highlight = highlight ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index aa378d0f..8f72bf9e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -1,8 +1,14 @@ package de.mm20.launcher2.ui.launcher.search +import android.content.Context 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.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import de.mm20.launcher2.favorites.FavoritesRepository 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.search.SavableSearchable 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.Calculator 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 kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -38,10 +51,12 @@ class SearchVM : ViewModel(), KoinComponent { private val permissionsManager: PermissionsManager 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() - val isSearching = MutableLiveData(false) - val searchQuery = MutableLiveData("") + val searchQuery = MutableLiveData("") val isSearchEmpty = MutableLiveData(true) val appResults = MutableLiveData>(emptyList()) @@ -63,18 +78,32 @@ class SearchVM : ViewModel(), KoinComponent { private val hiddenItemKeys = favoritesRepository .getHiddenItemKeys() - .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1) + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) + + val bestMatch = mutableStateOf(null) init { 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) { if (searchQuery.value == query && !forceRestart) return searchQuery.value = query isSearchEmpty.value = query.isEmpty() hiddenResults.value = emptyList() + bestMatch.value = null try { searchJob?.cancel() @@ -82,7 +111,6 @@ class SearchVM : ViewModel(), KoinComponent { } hideFavorites.postValue(query.isNotEmpty()) searchJob = viewModelScope.launch { - isSearching.postValue(true) dataStore.data.collectLatest { searchService.search( @@ -128,6 +156,20 @@ class SearchVM : ViewModel(), KoinComponent { 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 appResults.value = apps 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( permissionsManager.hasPermission(PermissionGroup.Contacts), dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt deleted file mode 100644 index 4b3ae73d..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt +++ /dev/null @@ -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) - } - } - -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/appshortcuts/AppShortcutResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/appshortcuts/AppShortcutResults.kt deleted file mode 100644 index 4fa61575..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/appshortcuts/AppShortcutResults.kt +++ /dev/null @@ -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 - ) - } - } - -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt deleted file mode 100644 index c927518e..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt +++ /dev/null @@ -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 - ) - } - } - } -} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt index 201811ce..f469886f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt @@ -4,13 +4,28 @@ import android.content.ComponentName import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateFloatAsState 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.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.Modifier import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.boundsInWindow 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.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import de.mm20.launcher2.search.SavableSearchable 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.LocalIconShape import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.ktx.toDp import de.mm20.launcher2.ui.ktx.toPixels @@ -45,13 +68,19 @@ import kotlinx.coroutines.delay @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 context = LocalContext.current var showPopup by remember(item.key) { mutableStateOf(false) } var bounds by remember { mutableStateOf(Rect.Zero) } + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { val badge by remember(item.key) { viewModel.badge }.collectAsState(null) val iconSize = LocalGridSettings.current.iconSize.dp.toPixels() @@ -72,7 +101,9 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels: return@HandleHomeTransition HomeTransitionParams( bounds ) { _, _ -> - ShapedLauncherIcon(size = LocalGridSettings.current.iconSize.dp, icon = { icon }) + ShapedLauncherIcon( + size = LocalGridSettings.current.iconSize.dp, + icon = { icon }) } } return@HandleHomeTransition null @@ -80,29 +111,42 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels: } val hapticFeedback = LocalHapticFeedback.current - ShapedLauncherIcon( - modifier = Modifier - .onGloballyPositioned { - bounds = it.boundsInWindow() + val iconShape = LocalIconShape.current + + Box( + 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, - badge = { badge }, - icon = { icon }, - onClick = { - if (!launchOnPress || !viewModel.launch(context, bounds)) { + onLongClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) showPopup = true } - }, - onLongClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - showPopup = true - } - ) + ) + } if (showLabels) { Text( modifier = Modifier .fillMaxWidth() - .padding(top = 8.dp), + .padding(vertical = 4.dp), text = item.labelOverride ?: item.label, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodySmall, @@ -110,6 +154,7 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels: overflow = TextOverflow.Ellipsis ) } + if (showPopup) { 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), ) .wrapContentSize() + .padding(4.dp) ) { when (searchable) { is LauncherApp -> { @@ -171,6 +217,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit } ) } + is Website -> { WebsiteItemGridPopup( website = searchable, @@ -182,6 +229,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit } ) } + is Wikipedia -> { WikipediaItemGridPopup( wikipedia = searchable, @@ -193,6 +241,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit } ) } + is Contact -> { ContactItemGridPopup( contact = searchable, @@ -204,6 +253,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit } ) } + is File -> { FileItemGridPopup( file = searchable, @@ -215,6 +265,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit } ) } + is CalendarEvent -> { CalendarItemGridPopup( calendar = searchable, @@ -226,6 +277,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit } ) } + is AppShortcut -> { ShortcutItemGridPopup( shortcut = searchable, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/SearchResultGrid.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/SearchResultGrid.kt index fa204cee..b111cf11 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/SearchResultGrid.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/SearchResultGrid.kt @@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.search.common.grid import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -16,7 +17,8 @@ fun SearchResultGrid( modifier: Modifier = Modifier, showLabels: Boolean = LocalGridSettings.current.showLabels, columns: Int = LocalGridSettings.current.columnCount, - reverse: Boolean = false + reverse: Boolean = false, + highlightedItem: SavableSearchable? = null ) { Column( modifier = modifier @@ -33,9 +35,10 @@ fun SearchResultGrid( GridItem( modifier = Modifier .weight(1f) - .padding(4.dp, 8.dp), + .padding(4.dp), item = item, - showLabels = showLabels + showLabels = showLabels, + highlight = item.key == highlightedItem?.key ) } else { Spacer(modifier = Modifier.weight(1f)) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt index 3f30592f..b643bd39 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt @@ -17,7 +17,11 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItem import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem @Composable -fun ListItem(modifier: Modifier = Modifier, item: SavableSearchable) { +fun ListItem( + modifier: Modifier = Modifier, + item: SavableSearchable, + highlight: Boolean = false +) { var showDetails by remember { mutableStateOf(false) } val context = LocalContext.current @@ -29,6 +33,7 @@ fun ListItem(modifier: Modifier = Modifier, item: SavableSearchable) { .onGloballyPositioned { bounds = it.boundsInWindow() }, + highlight = highlight, raised = showDetails ) { when (item) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt index 9c3613f4..4ee9f742 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt @@ -15,7 +15,8 @@ import de.mm20.launcher2.ui.layout.BottomReversed fun SearchResultList( items: List, modifier: Modifier = Modifier, - reverse: Boolean = false + reverse: Boolean = false, + highlightedItem: SavableSearchable? = null ) { Column( modifier = modifier, @@ -27,7 +28,8 @@ fun SearchResultList( modifier = Modifier .fillMaxWidth() .padding(4.dp), - item = item + item = item, + highlight = item.key == highlightedItem?.key ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt deleted file mode 100644 index b671bcd2..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt +++ /dev/null @@ -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 - ) - } - } - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt deleted file mode 100644 index bc026f3f..00000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt +++ /dev/null @@ -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 - )} - } - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt index 7ae4f670..2ac74def 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/LauncherSearchBar.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.foundation.text.KeyboardActionScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.FilledIconButton @@ -13,9 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.Modifier import androidx.compose.ui.focus.FocusRequester 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.ui.component.SearchBar 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.sheets.LocalBottomSheetManager @@ -38,9 +36,11 @@ fun LauncherSearchBar( focused: Boolean, onFocusChange: (Boolean) -> Unit, actions: List, + highlightedAction: SearchAction?, showHiddenItemsButton: Boolean = false, reverse: Boolean = false, darkColors: Boolean = false, + onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null ) { val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } @@ -51,7 +51,6 @@ fun LauncherSearchBar( val hiddenItems by searchVM.hiddenResults.observeAsState(emptyList()) - LaunchedEffect(focused) { if (focused) focusRequester.requestFocus() else focusManager.clearFocus() @@ -80,10 +79,11 @@ fun LauncherSearchBar( SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange) }, actions = { - SearchBarActions(actions = actions, reverse = reverse) + SearchBarActions(actions = actions, reverse = reverse, highlightedAction = highlightedAction) }, focusRequester = focusRequester, onFocus = { onFocusChange(true) }, onUnfocus = { onFocusChange(false) }, + onKeyboardActionGo = onKeyboardActionGo ) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt index f54457e6..30c81020 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt @@ -11,8 +11,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Edit import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,6 +30,7 @@ import de.mm20.launcher2.ui.settings.SettingsActivity fun ColumnScope.SearchBarActions( modifier: Modifier = Modifier, actions: List, + highlightedAction: SearchAction?, reverse: Boolean = false, ) { val context = LocalContext.current @@ -42,6 +45,16 @@ fun ColumnScope.SearchBarActions( items(actions) { AssistChip( 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 = { it.start(context) }, @@ -51,24 +64,6 @@ fun ColumnScope.SearchBarActions( 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 { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt index 2eeb9de3..67c6e6e6 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt @@ -453,7 +453,8 @@ fun SearchBarStylePreference( level = level, style = styles[it], value = previewSearchValue, - onValueChange = {}) + onValueChange = {} + ) } HorizontalPagerIndicator(pagerState = pagerState) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index 8c7b3a19..f074f34c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -181,14 +181,24 @@ fun SearchSettingsScreen() { } item { val autoFocus by viewModel.autoFocus.observeAsState() + val launchOnEnter by viewModel.launchOnEnter.observeAsState() PreferenceCategory { SwitchPreference( title = stringResource(R.string.preference_search_bar_auto_focus), summary = stringResource(R.string.preference_search_bar_auto_focus_summary), value = autoFocus == true, 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( title = stringResource(R.string.preference_hidden_items), summary = stringResource(R.string.preference_hidden_items_summary), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt index 7f676256..ff20495e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreenVM.kt @@ -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 appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData() diff --git a/core/i18n/src/main/res/values-de/strings.xml b/core/i18n/src/main/res/values-de/strings.xml index fdd72f38..5bf6f211 100644 --- a/core/i18n/src/main/res/values-de/strings.xml +++ b/core/i18n/src/main/res/values-de/strings.xml @@ -347,6 +347,8 @@ Erscheinungsbild der Suchleiste anpassen Tastatur öffnen Bildschirmtastatur beim Öffnen der Suche automatisch einblenden + Eingabe zum Starten + Hervorgehobenes Suchergebnis oder Schnellaktion beim Berühren der Start-Taste aufrufen Wikipedia-URL %1$s spielt Medien Bisher wurden keine Medien abgespielt @@ -572,4 +574,4 @@ Feste Bildschirmausrichtung Porträtmodus erzwingen Dynamische Farben - \ No newline at end of file + diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index c9e7ba48..f4412861 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -631,6 +631,8 @@ Color Open keyboard Automatically show the keyboard when opening the search + Launch on enter + Launch highlighted match or quick-action upon tapping go Hidden search results Manage hidden apps and search results Tags @@ -756,4 +758,4 @@ You have performed a \"%1$s\" gesture. This gesture is currently set to trigger a \"%2$s\" action. However, the action could not be performed for the following reason: The launcher\'s accessibility service needs to be enabled to perform this action. This action requires the launcher\'s accessibility service to be enabled. - \ No newline at end of file + diff --git a/core/preferences/src/main/proto/settings.proto b/core/preferences/src/main/proto/settings.proto index 71d51873..948dddf9 100644 --- a/core/preferences/src/main/proto/settings.proto +++ b/core/preferences/src/main/proto/settings.proto @@ -222,6 +222,7 @@ message Settings { Dark = 2; } SearchBarColors color = 3; + bool launch_on_enter = 4; } SearchBarSettings search_bar = 20;