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:
parent
72b42a1105
commit
b11ba9e23a
@ -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) },
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -228,13 +228,6 @@ fun ShapedLauncherIcon(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Badge(
|
||||
badge: () -> Badge?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconLayer(
|
||||
layer: LauncherIconLayer,
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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) },
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<SavableSearchable>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>("")
|
||||
val searchQuery = MutableLiveData("")
|
||||
val isSearchEmpty = MutableLiveData(true)
|
||||
|
||||
val appResults = MutableLiveData<List<LauncherApp>>(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<Searchable?>(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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -15,7 +15,8 @@ import de.mm20.launcher2.ui.layout.BottomReversed
|
||||
fun SearchResultList(
|
||||
items: List<SavableSearchable>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<SearchAction>,
|
||||
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
|
||||
)
|
||||
}
|
||||
@ -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<SearchAction>,
|
||||
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 {
|
||||
|
||||
@ -453,7 +453,8 @@ fun SearchBarStylePreference(
|
||||
level = level,
|
||||
style = styles[it],
|
||||
value = previewSearchValue,
|
||||
onValueChange = {})
|
||||
onValueChange = {}
|
||||
)
|
||||
}
|
||||
HorizontalPagerIndicator(pagerState = pagerState)
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -347,6 +347,8 @@
|
||||
<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_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="music_widget_default_title">%1$s spielt Medien</string>
|
||||
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>
|
||||
|
||||
@ -631,6 +631,8 @@
|
||||
<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_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_summary">Manage hidden apps and search results</string>
|
||||
<string name="preference_screen_tags">Tags</string>
|
||||
|
||||
@ -222,6 +222,7 @@ message Settings {
|
||||
Dark = 2;
|
||||
}
|
||||
SearchBarColors color = 3;
|
||||
bool launch_on_enter = 4;
|
||||
}
|
||||
SearchBarSettings search_bar = 20;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user