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 androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
|
import de.mm20.launcher2.ui.launcher.LauncherScaffoldVM
|
||||||
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
import de.mm20.launcher2.ui.launcher.helper.WallpaperBlur
|
||||||
@ -199,6 +200,7 @@ fun AssistantScaffold(
|
|||||||
viewModel.setSearchbarFocus(it)
|
viewModel.setSearchbarFocus(it)
|
||||||
},
|
},
|
||||||
actions = actions,
|
actions = actions,
|
||||||
|
highlightedAction = searchVM.bestMatch.value as? SearchAction,
|
||||||
showHiddenItemsButton = true,
|
showHiddenItemsButton = true,
|
||||||
value = { value },
|
value = { value },
|
||||||
onValueChange = { searchVM.search(it) },
|
onValueChange = { searchVM.search(it) },
|
||||||
|
|||||||
@ -8,11 +8,9 @@ import androidx.compose.animation.core.updateTransition
|
|||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -24,41 +22,56 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.compositeOver
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
|
||||||
import kotlin.math.ln
|
import kotlin.math.ln
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InnerCard(
|
fun InnerCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
raised: Boolean = false,
|
raised: Boolean = false,
|
||||||
|
highlight: Boolean = false,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val transition = updateTransition(raised, label = "InnerCard")
|
val transition = updateTransition(InnerCardStyle(raised, highlight), label = "InnerCard")
|
||||||
|
|
||||||
val absoluteTonalElevation = LocalAbsoluteTonalElevation.current
|
val absoluteTonalElevation = LocalAbsoluteTonalElevation.current
|
||||||
|
|
||||||
val elevation by transition.animateDp(label = "elevation", transitionSpec = {
|
val elevation by transition.animateDp(label = "elevation", transitionSpec = {
|
||||||
tween(250, if (targetState) 250 else 0)
|
tween(250, if (targetState == InnerCardStyle.Raised) 250 else 0)
|
||||||
}) {
|
}) {
|
||||||
if(it) 4.dp else 0.dp
|
if (it == InnerCardStyle.Raised) 2.dp else 0.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
val borderWidth by transition.animateDp(label = "borderWidth", transitionSpec = { tween(500) }) {
|
val borderWidth by transition.animateDp(
|
||||||
if (it) 0.dp else 1.dp
|
label = "borderWidth",
|
||||||
|
transitionSpec = { tween(500) }) {
|
||||||
|
when (it) {
|
||||||
|
InnerCardStyle.Raised -> 0.dp
|
||||||
|
InnerCardStyle.Highlighted -> 1.dp
|
||||||
|
InnerCardStyle.Default -> 1.dp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val borderColor by transition.animateColor(label = "borderColor", transitionSpec = { tween(500) }) {
|
val borderColor by transition.animateColor(
|
||||||
MaterialTheme.colorScheme.outline.copy(alpha = if (it) 0f else 0.7f)
|
label = "borderColor",
|
||||||
|
transitionSpec = { tween(500) }) {
|
||||||
|
when (it) {
|
||||||
|
InnerCardStyle.Raised -> Color.Transparent
|
||||||
|
InnerCardStyle.Highlighted -> MaterialTheme.colorScheme.secondary
|
||||||
|
InnerCardStyle.Default -> MaterialTheme.colorScheme.outlineVariant
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val bgAlpha by transition.animateFloat(label = "bgAlpha", transitionSpec = {
|
|
||||||
tween(250, if (targetState) 0 else 250)
|
val bgColor by transition.animateColor(label = "bgColor", transitionSpec = {
|
||||||
|
tween(250, if (targetState == InnerCardStyle.Raised) 0 else 250)
|
||||||
}) {
|
}) {
|
||||||
if (it) 1f else 0f
|
if (it == InnerCardStyle.Highlighted) {
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation + elevation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val shape = MaterialTheme.shapes.small
|
val shape = MaterialTheme.shapes.small
|
||||||
|
|
||||||
val bgColor = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteTonalElevation + elevation)
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.border(BorderStroke(borderWidth, borderColor), shape)
|
.border(BorderStroke(borderWidth, borderColor), shape)
|
||||||
@ -66,10 +79,9 @@ fun InnerCard(
|
|||||||
.clip(shape)
|
.clip(shape)
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
drawRect(
|
drawRect(
|
||||||
bgColor.copy(alpha = bgAlpha)
|
bgColor
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
,
|
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAbsoluteTonalElevation provides absoluteTonalElevation
|
LocalAbsoluteTonalElevation provides absoluteTonalElevation
|
||||||
@ -79,6 +91,20 @@ fun InnerCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum class InnerCardStyle {
|
||||||
|
Default,
|
||||||
|
Highlighted,
|
||||||
|
Raised,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun InnerCardStyle(raised: Boolean, highlight: Boolean): InnerCardStyle {
|
||||||
|
return when {
|
||||||
|
raised -> InnerCardStyle.Raised
|
||||||
|
highlight -> InnerCardStyle.Highlighted
|
||||||
|
else -> InnerCardStyle.Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun ColorScheme.surfaceColorAtElevation(
|
internal fun ColorScheme.surfaceColorAtElevation(
|
||||||
elevation: Dp,
|
elevation: Dp,
|
||||||
): Color {
|
): Color {
|
||||||
|
|||||||
@ -15,6 +15,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActionScope
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
@ -34,6 +37,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
@ -54,6 +58,7 @@ fun SearchBar(
|
|||||||
darkColors: Boolean = false,
|
darkColors: Boolean = false,
|
||||||
menu: @Composable RowScope.() -> Unit = {},
|
menu: @Composable RowScope.() -> Unit = {},
|
||||||
actions: @Composable ColumnScope.() -> Unit = {},
|
actions: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val transition = updateTransition(level, label = "Searchbar")
|
val transition = updateTransition(level, label = "Searchbar")
|
||||||
@ -171,7 +176,17 @@ fun SearchBar(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary)
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = if (onKeyboardActionGo == null) {
|
||||||
|
ImeAction.Search
|
||||||
|
} else {
|
||||||
|
ImeAction.Go
|
||||||
|
}
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onGo = onKeyboardActionGo
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@ -228,13 +228,6 @@ fun ShapedLauncherIcon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Badge(
|
|
||||||
badge: () -> Badge?
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IconLayer(
|
private fun IconLayer(
|
||||||
layer: LauncherIconLayer,
|
layer: LauncherIconLayer,
|
||||||
|
|||||||
@ -56,6 +56,8 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
val statusBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
|
val statusBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
|
||||||
val navBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
|
val navBarColor = dataStore.data.map { it.systemBars.statusBarColor }.asLiveData()
|
||||||
|
|
||||||
|
val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }.asLiveData()
|
||||||
|
|
||||||
val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }.asLiveData()
|
val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }.asLiveData()
|
||||||
val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }.asLiveData()
|
val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }.asLiveData()
|
||||||
|
|
||||||
|
|||||||
@ -69,6 +69,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarColors
|
||||||
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
import de.mm20.launcher2.preferences.Settings.SearchBarSettings.SearchBarStyle
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
|
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
|
||||||
@ -503,6 +504,7 @@ fun PagerScaffold(
|
|||||||
viewModel.setSearchbarFocus(it)
|
viewModel.setSearchbarFocus(it)
|
||||||
},
|
},
|
||||||
actions = actions,
|
actions = actions,
|
||||||
|
highlightedAction = searchVM.bestMatch.value as? SearchAction,
|
||||||
showHiddenItemsButton = isSearchOpen,
|
showHiddenItemsButton = isSearchOpen,
|
||||||
value = { value },
|
value = { value },
|
||||||
onValueChange = { searchVM.search(it) },
|
onValueChange = { searchVM.search(it) },
|
||||||
|
|||||||
@ -56,6 +56,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
@ -66,6 +67,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
|
import de.mm20.launcher2.ui.gestures.LocalGestureDetector
|
||||||
@ -89,6 +91,7 @@ fun PullDownScaffold(
|
|||||||
bottomSearchBar: Boolean = false,
|
bottomSearchBar: Boolean = false,
|
||||||
reverseSearchResults: Boolean = false,
|
reverseSearchResults: Boolean = false,
|
||||||
fixedSearchBar: Boolean = false,
|
fixedSearchBar: Boolean = false,
|
||||||
|
launchOnEnter: Boolean = false
|
||||||
) {
|
) {
|
||||||
val viewModel: LauncherScaffoldVM = viewModel()
|
val viewModel: LauncherScaffoldVM = viewModel()
|
||||||
val searchVM: SearchVM = viewModel()
|
val searchVM: SearchVM = viewModel()
|
||||||
@ -394,9 +397,13 @@ fun PullDownScaffold(
|
|||||||
}
|
}
|
||||||
.pointerInput(gestureManager.shouldDetectDoubleTaps) {
|
.pointerInput(gestureManager.shouldDetectDoubleTaps) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) {{
|
onDoubleTap = if (gestureManager.shouldDetectDoubleTaps) {
|
||||||
if (!isWidgetEditMode) gestureManager.dispatchDoubleTap(it)
|
{
|
||||||
}} else null,
|
if (!isWidgetEditMode) gestureManager.dispatchDoubleTap(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
if (!isWidgetEditMode) gestureManager.dispatchLongPress(
|
if (!isWidgetEditMode) gestureManager.dispatchLongPress(
|
||||||
it
|
it
|
||||||
@ -518,6 +525,8 @@ fun PullDownScaffold(
|
|||||||
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
val searchBarColor by viewModel.searchBarColor.observeAsState(Settings.SearchBarSettings.SearchBarColors.Auto)
|
||||||
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
val searchBarStyle by viewModel.searchBarStyle.observeAsState(Settings.SearchBarSettings.SearchBarStyle.Transparent)
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
LauncherSearchBar(
|
LauncherSearchBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter)
|
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter)
|
||||||
@ -547,12 +556,16 @@ fun PullDownScaffold(
|
|||||||
viewModel.setSearchbarFocus(it)
|
viewModel.setSearchbarFocus(it)
|
||||||
},
|
},
|
||||||
actions = actions,
|
actions = actions,
|
||||||
|
highlightedAction = searchVM.bestMatch.value as? SearchAction,
|
||||||
showHiddenItemsButton = isSearchOpen,
|
showHiddenItemsButton = isSearchOpen,
|
||||||
value = { value },
|
value = { value },
|
||||||
onValueChange = { searchVM.search(it) },
|
onValueChange = { searchVM.search(it) },
|
||||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == Settings.SearchBarSettings.SearchBarColors.Auto || searchBarColor == Settings.SearchBarSettings.SearchBarColors.Dark,
|
||||||
style = searchBarStyle,
|
style = searchBarStyle,
|
||||||
reverse = bottomSearchBar,
|
reverse = bottomSearchBar,
|
||||||
|
onKeyboardActionGo = if (launchOnEnter) {
|
||||||
|
{ searchVM.launchBestMatchOrAction(context) }
|
||||||
|
} else null
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,6 +194,7 @@ abstract class SharedLauncherActivity(
|
|||||||
when (layout) {
|
when (layout) {
|
||||||
Settings.LayoutSettings.Layout.PullDown -> {
|
Settings.LayoutSettings.Layout.PullDown -> {
|
||||||
key(bottomSearchBar, reverseSearchResults) {
|
key(bottomSearchBar, reverseSearchResults) {
|
||||||
|
val launchOnEnter by viewModel.launchOnEnter.observeAsState(false)
|
||||||
PullDownScaffold(
|
PullDownScaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -209,6 +210,7 @@ abstract class SharedLauncherActivity(
|
|||||||
bottomSearchBar = bottomSearchBar,
|
bottomSearchBar = bottomSearchBar,
|
||||||
reverseSearchResults = reverseSearchResults,
|
reverseSearchResults = reverseSearchResults,
|
||||||
fixedSearchBar = fixedSearchBar,
|
fixedSearchBar = fixedSearchBar,
|
||||||
|
launchOnEnter = launchOnEnter
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,6 +94,8 @@ fun SearchColumn(
|
|||||||
val website by viewModel.websiteResults.observeAsState(emptyList())
|
val website by viewModel.websiteResults.observeAsState(emptyList())
|
||||||
val hiddenResults by viewModel.hiddenResults.observeAsState(emptyList())
|
val hiddenResults by viewModel.hiddenResults.observeAsState(emptyList())
|
||||||
|
|
||||||
|
val bestMatch by viewModel.bestMatch
|
||||||
|
|
||||||
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
|
val isSearchEmpty by viewModel.isSearchEmpty.observeAsState(true)
|
||||||
|
|
||||||
val missingCalendarPermission by viewModel.missingCalendarPermission.collectAsState(false)
|
val missingCalendarPermission by viewModel.missingCalendarPermission.collectAsState(false)
|
||||||
@ -191,7 +193,8 @@ fun SearchColumn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
highlightedItem = bestMatch as? SavableSearchable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GridResults(
|
GridResults(
|
||||||
@ -241,7 +244,8 @@ fun SearchColumn(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else null
|
} else null,
|
||||||
|
highlightedItem = bestMatch as? SavableSearchable
|
||||||
)
|
)
|
||||||
ListResults(
|
ListResults(
|
||||||
before = if (missingShortcutsPermission && !isSearchEmpty) {
|
before = if (missingShortcutsPermission && !isSearchEmpty) {
|
||||||
@ -267,7 +271,8 @@ fun SearchColumn(
|
|||||||
} else null,
|
} else null,
|
||||||
items = appShortcuts.toImmutableList(),
|
items = appShortcuts.toImmutableList(),
|
||||||
reverse = reverse,
|
reverse = reverse,
|
||||||
key = "shortcuts"
|
key = "shortcuts",
|
||||||
|
highlightedItem = bestMatch as? SavableSearchable
|
||||||
)
|
)
|
||||||
for (conv in unitConverter) {
|
for (conv in unitConverter) {
|
||||||
SingleResult {
|
SingleResult {
|
||||||
@ -300,7 +305,8 @@ fun SearchColumn(
|
|||||||
} else null,
|
} else null,
|
||||||
items = events.toImmutableList(),
|
items = events.toImmutableList(),
|
||||||
reverse = reverse,
|
reverse = reverse,
|
||||||
key = "events"
|
key = "events",
|
||||||
|
highlightedItem = bestMatch as? SavableSearchable
|
||||||
)
|
)
|
||||||
ListResults(
|
ListResults(
|
||||||
before = if (missingContactsPermission && !isSearchEmpty) {
|
before = if (missingContactsPermission && !isSearchEmpty) {
|
||||||
@ -323,7 +329,8 @@ fun SearchColumn(
|
|||||||
} else null,
|
} else null,
|
||||||
items = contacts.toImmutableList(),
|
items = contacts.toImmutableList(),
|
||||||
reverse = reverse,
|
reverse = reverse,
|
||||||
key = "contacts"
|
key = "contacts",
|
||||||
|
highlightedItem = bestMatch as? SavableSearchable
|
||||||
)
|
)
|
||||||
for (wiki in wikipedia) {
|
for (wiki in wikipedia) {
|
||||||
SingleResult {
|
SingleResult {
|
||||||
@ -356,7 +363,8 @@ fun SearchColumn(
|
|||||||
} else null,
|
} else null,
|
||||||
items = files.toImmutableList(),
|
items = files.toImmutableList(),
|
||||||
reverse = reverse,
|
reverse = reverse,
|
||||||
key = "files"
|
key = "files",
|
||||||
|
highlightedItem = bestMatch as? SavableSearchable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,6 +381,7 @@ fun LazyListScope.GridResults(
|
|||||||
key: String,
|
key: String,
|
||||||
before: (@Composable () -> Unit)? = null,
|
before: (@Composable () -> Unit)? = null,
|
||||||
after: (@Composable () -> Unit)? = null,
|
after: (@Composable () -> Unit)? = null,
|
||||||
|
highlightedItem: SavableSearchable?
|
||||||
) {
|
) {
|
||||||
if (items.isEmpty() && before == null && after == null) return
|
if (items.isEmpty() && before == null && after == null) return
|
||||||
|
|
||||||
@ -410,6 +419,7 @@ fun LazyListScope.GridResults(
|
|||||||
(it * columns + columns).coerceAtMost(items.size)
|
(it * columns + columns).coerceAtMost(items.size)
|
||||||
),
|
),
|
||||||
columns = columns,
|
columns = columns,
|
||||||
|
highlightedItem = highlightedItem
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -433,6 +443,7 @@ fun GridRow(
|
|||||||
items: ImmutableList<SavableSearchable>,
|
items: ImmutableList<SavableSearchable>,
|
||||||
columns: Int,
|
columns: Int,
|
||||||
showLabels: Boolean = LocalGridSettings.current.showLabels,
|
showLabels: Boolean = LocalGridSettings.current.showLabels,
|
||||||
|
highlightedItem: SavableSearchable?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@ -442,9 +453,10 @@ fun GridRow(
|
|||||||
GridItem(
|
GridItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(4.dp, 8.dp),
|
.padding(4.dp),
|
||||||
item = item,
|
item = item,
|
||||||
showLabels = showLabels
|
showLabels = showLabels,
|
||||||
|
highlight = item.key == highlightedItem?.key
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (i in 0 until columns - items.size) {
|
for (i in 0 until columns - items.size) {
|
||||||
@ -459,6 +471,7 @@ fun LazyListScope.ListResults(
|
|||||||
key: String,
|
key: String,
|
||||||
before: (@Composable () -> Unit)? = null,
|
before: (@Composable () -> Unit)? = null,
|
||||||
after: (@Composable () -> Unit)? = null,
|
after: (@Composable () -> Unit)? = null,
|
||||||
|
highlightedItem: SavableSearchable?
|
||||||
) {
|
) {
|
||||||
if (before != null) {
|
if (before != null) {
|
||||||
item(key = "$key-before") {
|
item(key = "$key-before") {
|
||||||
@ -488,6 +501,7 @@ fun LazyListScope.ListResults(
|
|||||||
bottom = if (if (reverse) it == 0 else it == items.size - 1) 8.dp else 4.dp,
|
bottom = if (if (reverse) it == 0 else it == items.size - 1) 8.dp else 4.dp,
|
||||||
),
|
),
|
||||||
item = items[it],
|
item = items[it],
|
||||||
|
highlight = items[it].key == highlightedItem?.key
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -508,6 +522,7 @@ fun LazyListScope.ListResults(
|
|||||||
fun ListRow(
|
fun ListRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: SavableSearchable,
|
item: SavableSearchable,
|
||||||
|
highlight: Boolean
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.padding(
|
modifier = modifier.padding(
|
||||||
@ -518,7 +533,8 @@ fun ListRow(
|
|||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
item = item
|
item = item,
|
||||||
|
highlight = highlight
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.permissions.PermissionGroup
|
import de.mm20.launcher2.permissions.PermissionGroup
|
||||||
@ -10,6 +16,7 @@ import de.mm20.launcher2.permissions.PermissionsManager
|
|||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.search.SearchService
|
import de.mm20.launcher2.search.SearchService
|
||||||
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.Calculator
|
import de.mm20.launcher2.search.data.Calculator
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
@ -22,12 +29,18 @@ import de.mm20.launcher2.search.data.Wikipedia
|
|||||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
@ -38,10 +51,12 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
private val permissionsManager: PermissionsManager by inject()
|
private val permissionsManager: PermissionsManager by inject()
|
||||||
private val dataStore: LauncherDataStore by inject()
|
private val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
|
private val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
private val searchService: SearchService by inject()
|
private val searchService: SearchService by inject()
|
||||||
|
|
||||||
val isSearching = MutableLiveData(false)
|
val searchQuery = MutableLiveData("")
|
||||||
val searchQuery = MutableLiveData<String>("")
|
|
||||||
val isSearchEmpty = MutableLiveData(true)
|
val isSearchEmpty = MutableLiveData(true)
|
||||||
|
|
||||||
val appResults = MutableLiveData<List<LauncherApp>>(emptyList())
|
val appResults = MutableLiveData<List<LauncherApp>>(emptyList())
|
||||||
@ -63,18 +78,32 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
private val hiddenItemKeys = favoritesRepository
|
private val hiddenItemKeys = favoritesRepository
|
||||||
.getHiddenItemKeys()
|
.getHiddenItemKeys()
|
||||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
|
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
|
val bestMatch = mutableStateOf<Searchable?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
search("", true)
|
search("", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchJob: Job? = null
|
fun launchBestMatchOrAction(context: Context) {
|
||||||
|
val bestMatch = bestMatch.value
|
||||||
|
if (bestMatch is SavableSearchable) {
|
||||||
|
bestMatch.launch(context, null)
|
||||||
|
return
|
||||||
|
} else if (bestMatch is SearchAction) {
|
||||||
|
bestMatch.start(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var searchJob: Job? = null
|
||||||
fun search(query: String, forceRestart: Boolean = false) {
|
fun search(query: String, forceRestart: Boolean = false) {
|
||||||
if (searchQuery.value == query && !forceRestart) return
|
if (searchQuery.value == query && !forceRestart) return
|
||||||
searchQuery.value = query
|
searchQuery.value = query
|
||||||
isSearchEmpty.value = query.isEmpty()
|
isSearchEmpty.value = query.isEmpty()
|
||||||
hiddenResults.value = emptyList()
|
hiddenResults.value = emptyList()
|
||||||
|
bestMatch.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
searchJob?.cancel()
|
searchJob?.cancel()
|
||||||
@ -82,7 +111,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
hideFavorites.postValue(query.isNotEmpty())
|
hideFavorites.postValue(query.isNotEmpty())
|
||||||
searchJob = viewModelScope.launch {
|
searchJob = viewModelScope.launch {
|
||||||
isSearching.postValue(true)
|
|
||||||
|
|
||||||
dataStore.data.collectLatest {
|
dataStore.data.collectLatest {
|
||||||
searchService.search(
|
searchService.search(
|
||||||
@ -128,6 +156,20 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
r is SearchAction -> actions.add(r)
|
r is SearchAction -> actions.add(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (query.isNotEmpty() && launchOnEnter.value) {
|
||||||
|
bestMatch.value = listOf(
|
||||||
|
apps,
|
||||||
|
workApps,
|
||||||
|
shortcuts,
|
||||||
|
files,
|
||||||
|
contacts,
|
||||||
|
events,
|
||||||
|
wikipedia,
|
||||||
|
website,
|
||||||
|
actions
|
||||||
|
).firstNotNullOfOrNull { it.firstOrNull() }
|
||||||
|
}
|
||||||
|
|
||||||
searchActionResults.value = actions
|
searchActionResults.value = actions
|
||||||
appResults.value = apps
|
appResults.value = apps
|
||||||
workAppResults.value = workApps
|
workAppResults.value = workApps
|
||||||
@ -143,9 +185,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
isSearching.postValue(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +207,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val missingContactsPermission = combine(
|
val missingContactsPermission = combine(
|
||||||
permissionsManager.hasPermission(PermissionGroup.Contacts),
|
permissionsManager.hasPermission(PermissionGroup.Contacts),
|
||||||
dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged()
|
dataStore.data.map { it.contactsSearch.enabled }.distinctUntilChanged()
|
||||||
|
|||||||
@ -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.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.absoluteOffset
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
@ -18,14 +33,22 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.search.Searchable
|
import de.mm20.launcher2.search.Searchable
|
||||||
import de.mm20.launcher2.search.data.*
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
|
import de.mm20.launcher2.search.data.Contact
|
||||||
|
import de.mm20.launcher2.search.data.File
|
||||||
|
import de.mm20.launcher2.search.data.LauncherApp
|
||||||
|
import de.mm20.launcher2.search.data.Website
|
||||||
|
import de.mm20.launcher2.search.data.Wikipedia
|
||||||
import de.mm20.launcher2.ui.component.LauncherCard
|
import de.mm20.launcher2.ui.component.LauncherCard
|
||||||
|
import de.mm20.launcher2.ui.component.LocalIconShape
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
@ -45,13 +68,19 @@ import kotlinx.coroutines.delay
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels: Boolean = true) {
|
fun GridItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
item: SavableSearchable,
|
||||||
|
showLabels: Boolean = true,
|
||||||
|
highlight: Boolean = false
|
||||||
|
) {
|
||||||
val viewModel = remember(item.key) { GridItemVM(item) }
|
val viewModel = remember(item.key) { GridItemVM(item) }
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
var showPopup by remember(item.key) { mutableStateOf(false) }
|
var showPopup by remember(item.key) { mutableStateOf(false) }
|
||||||
var bounds by remember { mutableStateOf(Rect.Zero) }
|
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||||
|
|
||||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
val badge by remember(item.key) { viewModel.badge }.collectAsState(null)
|
val badge by remember(item.key) { viewModel.badge }.collectAsState(null)
|
||||||
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
|
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
|
||||||
@ -72,7 +101,9 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels:
|
|||||||
return@HandleHomeTransition HomeTransitionParams(
|
return@HandleHomeTransition HomeTransitionParams(
|
||||||
bounds
|
bounds
|
||||||
) { _, _ ->
|
) { _, _ ->
|
||||||
ShapedLauncherIcon(size = LocalGridSettings.current.iconSize.dp, icon = { icon })
|
ShapedLauncherIcon(
|
||||||
|
size = LocalGridSettings.current.iconSize.dp,
|
||||||
|
icon = { icon })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@HandleHomeTransition null
|
return@HandleHomeTransition null
|
||||||
@ -80,29 +111,42 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels:
|
|||||||
}
|
}
|
||||||
|
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
ShapedLauncherIcon(
|
val iconShape = LocalIconShape.current
|
||||||
modifier = Modifier
|
|
||||||
.onGloballyPositioned {
|
Box(
|
||||||
bounds = it.boundsInWindow()
|
modifier = if (highlight) {
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
iconShape
|
||||||
|
)
|
||||||
|
} else Modifier,
|
||||||
|
) {
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.onGloballyPositioned {
|
||||||
|
bounds = it.boundsInWindow()
|
||||||
|
},
|
||||||
|
size = LocalGridSettings.current.iconSize.dp,
|
||||||
|
badge = { badge },
|
||||||
|
icon = { icon },
|
||||||
|
onClick = {
|
||||||
|
if (!launchOnPress || !viewModel.launch(context, bounds)) {
|
||||||
|
showPopup = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
size = LocalGridSettings.current.iconSize.dp,
|
onLongClick = {
|
||||||
badge = { badge },
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
icon = { icon },
|
|
||||||
onClick = {
|
|
||||||
if (!launchOnPress || !viewModel.launch(context, bounds)) {
|
|
||||||
showPopup = true
|
showPopup = true
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
onLongClick = {
|
}
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
showPopup = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (showLabels) {
|
if (showLabels) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 8.dp),
|
.padding(vertical = 4.dp),
|
||||||
text = item.labelOverride ?: item.label,
|
text = item.labelOverride ?: item.label,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@ -110,6 +154,7 @@ fun GridItem(modifier: Modifier = Modifier, item: SavableSearchable, showLabels:
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPopup) {
|
if (showPopup) {
|
||||||
ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false })
|
ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false })
|
||||||
}
|
}
|
||||||
@ -158,6 +203,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
x = ((1 - animationProgress) * origin.left).toDp() - 16.dp * (1 - animationProgress),
|
x = ((1 - animationProgress) * origin.left).toDp() - 16.dp * (1 - animationProgress),
|
||||||
)
|
)
|
||||||
.wrapContentSize()
|
.wrapContentSize()
|
||||||
|
.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
when (searchable) {
|
when (searchable) {
|
||||||
is LauncherApp -> {
|
is LauncherApp -> {
|
||||||
@ -171,6 +217,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Website -> {
|
is Website -> {
|
||||||
WebsiteItemGridPopup(
|
WebsiteItemGridPopup(
|
||||||
website = searchable,
|
website = searchable,
|
||||||
@ -182,6 +229,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Wikipedia -> {
|
is Wikipedia -> {
|
||||||
WikipediaItemGridPopup(
|
WikipediaItemGridPopup(
|
||||||
wikipedia = searchable,
|
wikipedia = searchable,
|
||||||
@ -193,6 +241,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Contact -> {
|
is Contact -> {
|
||||||
ContactItemGridPopup(
|
ContactItemGridPopup(
|
||||||
contact = searchable,
|
contact = searchable,
|
||||||
@ -204,6 +253,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is File -> {
|
is File -> {
|
||||||
FileItemGridPopup(
|
FileItemGridPopup(
|
||||||
file = searchable,
|
file = searchable,
|
||||||
@ -215,6 +265,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CalendarEvent -> {
|
is CalendarEvent -> {
|
||||||
CalendarItemGridPopup(
|
CalendarItemGridPopup(
|
||||||
calendar = searchable,
|
calendar = searchable,
|
||||||
@ -226,6 +277,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppShortcut -> {
|
is AppShortcut -> {
|
||||||
ShortcutItemGridPopup(
|
ShortcutItemGridPopup(
|
||||||
shortcut = searchable,
|
shortcut = searchable,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.search.common.grid
|
|||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -16,7 +17,8 @@ fun SearchResultGrid(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showLabels: Boolean = LocalGridSettings.current.showLabels,
|
showLabels: Boolean = LocalGridSettings.current.showLabels,
|
||||||
columns: Int = LocalGridSettings.current.columnCount,
|
columns: Int = LocalGridSettings.current.columnCount,
|
||||||
reverse: Boolean = false
|
reverse: Boolean = false,
|
||||||
|
highlightedItem: SavableSearchable? = null
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -33,9 +35,10 @@ fun SearchResultGrid(
|
|||||||
GridItem(
|
GridItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(4.dp, 8.dp),
|
.padding(4.dp),
|
||||||
item = item,
|
item = item,
|
||||||
showLabels = showLabels
|
showLabels = showLabels,
|
||||||
|
highlight = item.key == highlightedItem?.key
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|||||||
@ -17,7 +17,11 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItem
|
|||||||
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
|
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListItem(modifier: Modifier = Modifier, item: SavableSearchable) {
|
fun ListItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
item: SavableSearchable,
|
||||||
|
highlight: Boolean = false
|
||||||
|
) {
|
||||||
var showDetails by remember { mutableStateOf(false) }
|
var showDetails by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@ -29,6 +33,7 @@ fun ListItem(modifier: Modifier = Modifier, item: SavableSearchable) {
|
|||||||
.onGloballyPositioned {
|
.onGloballyPositioned {
|
||||||
bounds = it.boundsInWindow()
|
bounds = it.boundsInWindow()
|
||||||
},
|
},
|
||||||
|
highlight = highlight,
|
||||||
raised = showDetails
|
raised = showDetails
|
||||||
) {
|
) {
|
||||||
when (item) {
|
when (item) {
|
||||||
|
|||||||
@ -15,7 +15,8 @@ import de.mm20.launcher2.ui.layout.BottomReversed
|
|||||||
fun SearchResultList(
|
fun SearchResultList(
|
||||||
items: List<SavableSearchable>,
|
items: List<SavableSearchable>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
reverse: Boolean = false
|
reverse: Boolean = false,
|
||||||
|
highlightedItem: SavableSearchable? = null
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@ -27,7 +28,8 @@ fun SearchResultList(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(4.dp),
|
.padding(4.dp),
|
||||||
item = item
|
item = item,
|
||||||
|
highlight = item.key == highlightedItem?.key
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.core.tween
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.foundation.text.KeyboardActionScope
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||||
import androidx.compose.material3.FilledIconButton
|
import androidx.compose.material3.FilledIconButton
|
||||||
@ -13,9 +14,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
@ -24,7 +23,6 @@ import de.mm20.launcher2.preferences.Settings
|
|||||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import de.mm20.launcher2.ui.component.SearchBar
|
import de.mm20.launcher2.ui.component.SearchBar
|
||||||
import de.mm20.launcher2.ui.component.SearchBarLevel
|
import de.mm20.launcher2.ui.component.SearchBarLevel
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
|
||||||
|
|
||||||
@ -38,9 +36,11 @@ fun LauncherSearchBar(
|
|||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
onFocusChange: (Boolean) -> Unit,
|
onFocusChange: (Boolean) -> Unit,
|
||||||
actions: List<SearchAction>,
|
actions: List<SearchAction>,
|
||||||
|
highlightedAction: SearchAction?,
|
||||||
showHiddenItemsButton: Boolean = false,
|
showHiddenItemsButton: Boolean = false,
|
||||||
reverse: Boolean = false,
|
reverse: Boolean = false,
|
||||||
darkColors: Boolean = false,
|
darkColors: Boolean = false,
|
||||||
|
onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@ -51,7 +51,6 @@ fun LauncherSearchBar(
|
|||||||
|
|
||||||
val hiddenItems by searchVM.hiddenResults.observeAsState(emptyList())
|
val hiddenItems by searchVM.hiddenResults.observeAsState(emptyList())
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(focused) {
|
LaunchedEffect(focused) {
|
||||||
if (focused) focusRequester.requestFocus()
|
if (focused) focusRequester.requestFocus()
|
||||||
else focusManager.clearFocus()
|
else focusManager.clearFocus()
|
||||||
@ -80,10 +79,11 @@ fun LauncherSearchBar(
|
|||||||
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
|
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
SearchBarActions(actions = actions, reverse = reverse)
|
SearchBarActions(actions = actions, reverse = reverse, highlightedAction = highlightedAction)
|
||||||
},
|
},
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
onFocus = { onFocusChange(true) },
|
onFocus = { onFocusChange(true) },
|
||||||
onUnfocus = { onFocusChange(false) },
|
onUnfocus = { onFocusChange(false) },
|
||||||
|
onKeyboardActionGo = onKeyboardActionGo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -11,8 +11,10 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
import androidx.compose.material3.AssistChip
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.AssistChipDefaults
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SmallFloatingActionButton
|
import androidx.compose.material3.SmallFloatingActionButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -28,6 +30,7 @@ import de.mm20.launcher2.ui.settings.SettingsActivity
|
|||||||
fun ColumnScope.SearchBarActions(
|
fun ColumnScope.SearchBarActions(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
actions: List<SearchAction>,
|
actions: List<SearchAction>,
|
||||||
|
highlightedAction: SearchAction?,
|
||||||
reverse: Boolean = false,
|
reverse: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -42,6 +45,16 @@ fun ColumnScope.SearchBarActions(
|
|||||||
items(actions) {
|
items(actions) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
modifier = Modifier.padding(4.dp),
|
modifier = Modifier.padding(4.dp),
|
||||||
|
colors = if (it == highlightedAction) {
|
||||||
|
AssistChipDefaults.assistChipColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
)
|
||||||
|
} else AssistChipDefaults.assistChipColors(),
|
||||||
|
border = if (it == highlightedAction) {
|
||||||
|
AssistChipDefaults.assistChipBorder(
|
||||||
|
borderColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
)
|
||||||
|
} else AssistChipDefaults.assistChipBorder(),
|
||||||
onClick = {
|
onClick = {
|
||||||
it.start(context)
|
it.start(context)
|
||||||
},
|
},
|
||||||
@ -51,24 +64,6 @@ fun ColumnScope.SearchBarActions(
|
|||||||
action = it
|
action = it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/*leadingIcon = {
|
|
||||||
val icon = it.icon
|
|
||||||
if (icon == null) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Search,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (it.color == 0) MaterialTheme.colorScheme.primary else Color(
|
|
||||||
it.color
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AsyncImage(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
model = File(icon),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
|
|||||||
@ -453,7 +453,8 @@ fun SearchBarStylePreference(
|
|||||||
level = level,
|
level = level,
|
||||||
style = styles[it],
|
style = styles[it],
|
||||||
value = previewSearchValue,
|
value = previewSearchValue,
|
||||||
onValueChange = {})
|
onValueChange = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
HorizontalPagerIndicator(pagerState = pagerState)
|
HorizontalPagerIndicator(pagerState = pagerState)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -181,14 +181,24 @@ fun SearchSettingsScreen() {
|
|||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
val autoFocus by viewModel.autoFocus.observeAsState()
|
val autoFocus by viewModel.autoFocus.observeAsState()
|
||||||
|
val launchOnEnter by viewModel.launchOnEnter.observeAsState()
|
||||||
PreferenceCategory {
|
PreferenceCategory {
|
||||||
SwitchPreference(
|
SwitchPreference(
|
||||||
title = stringResource(R.string.preference_search_bar_auto_focus),
|
title = stringResource(R.string.preference_search_bar_auto_focus),
|
||||||
summary = stringResource(R.string.preference_search_bar_auto_focus_summary),
|
summary = stringResource(R.string.preference_search_bar_auto_focus_summary),
|
||||||
value = autoFocus == true,
|
value = autoFocus == true,
|
||||||
onValueChanged = {
|
onValueChanged = {
|
||||||
viewModel.setAutoFocus(it)
|
viewModel.setAutoFocus(it)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
SwitchPreference(
|
||||||
|
title = stringResource(R.string.preference_search_bar_launch_on_enter),
|
||||||
|
summary = stringResource(R.string.preference_search_bar_launch_on_enter_summary),
|
||||||
|
value = launchOnEnter == true,
|
||||||
|
onValueChanged = {
|
||||||
|
viewModel.setLaunchOnEnter(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
Preference(
|
Preference(
|
||||||
title = stringResource(R.string.preference_hidden_items),
|
title = stringResource(R.string.preference_hidden_items),
|
||||||
summary = stringResource(R.string.preference_hidden_items_summary),
|
summary = stringResource(R.string.preference_hidden_items_summary),
|
||||||
|
|||||||
@ -151,6 +151,20 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val launchOnEnter = dataStore.data.map { it.searchBar.launchOnEnter }.asLiveData()
|
||||||
|
fun setLaunchOnEnter(launchOnEnter: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
dataStore.updateData {
|
||||||
|
it.toBuilder()
|
||||||
|
.setSearchBar(
|
||||||
|
it.searchBar.toBuilder()
|
||||||
|
.setLaunchOnEnter(launchOnEnter)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData()
|
val hasAppShortcutPermission = permissionsManager.hasPermission(PermissionGroup.AppShortcuts).asLiveData()
|
||||||
val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData()
|
val appShortcuts = dataStore.data.map { it.appShortcutSearch.enabled }.asLiveData()
|
||||||
|
|||||||
@ -347,6 +347,8 @@
|
|||||||
<string name="preference_search_bar_style_summary">Erscheinungsbild der Suchleiste anpassen</string>
|
<string name="preference_search_bar_style_summary">Erscheinungsbild der Suchleiste anpassen</string>
|
||||||
<string name="preference_search_bar_auto_focus">Tastatur öffnen</string>
|
<string name="preference_search_bar_auto_focus">Tastatur öffnen</string>
|
||||||
<string name="preference_search_bar_auto_focus_summary">Bildschirmtastatur beim Öffnen der Suche automatisch einblenden</string>
|
<string name="preference_search_bar_auto_focus_summary">Bildschirmtastatur beim Öffnen der Suche automatisch einblenden</string>
|
||||||
|
<string name="preference_search_bar_launch_on_enter">Eingabe zum Starten</string>
|
||||||
|
<string name="preference_search_bar_launch_on_enter_summary">Hervorgehobenes Suchergebnis oder Schnellaktion beim Berühren der Start-Taste aufrufen</string>
|
||||||
<string name="preference_wikipedia_customurl">Wikipedia-URL</string>
|
<string name="preference_wikipedia_customurl">Wikipedia-URL</string>
|
||||||
<string name="music_widget_default_title">%1$s spielt Medien</string>
|
<string name="music_widget_default_title">%1$s spielt Medien</string>
|
||||||
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>
|
<string name="music_widget_no_data">Bisher wurden keine Medien abgespielt</string>
|
||||||
@ -572,4 +574,4 @@
|
|||||||
<string name="preference_layout_fixed_rotation">Feste Bildschirmausrichtung</string>
|
<string name="preference_layout_fixed_rotation">Feste Bildschirmausrichtung</string>
|
||||||
<string name="preference_layout_fixed_rotation_summary">Porträtmodus erzwingen</string>
|
<string name="preference_layout_fixed_rotation_summary">Porträtmodus erzwingen</string>
|
||||||
<string name="icon_pack_dynamic_colors">Dynamische Farben</string>
|
<string name="icon_pack_dynamic_colors">Dynamische Farben</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -631,6 +631,8 @@
|
|||||||
<string name="preference_search_bar_color">Color</string>
|
<string name="preference_search_bar_color">Color</string>
|
||||||
<string name="preference_search_bar_auto_focus">Open keyboard</string>
|
<string name="preference_search_bar_auto_focus">Open keyboard</string>
|
||||||
<string name="preference_search_bar_auto_focus_summary">Automatically show the keyboard when opening the search</string>
|
<string name="preference_search_bar_auto_focus_summary">Automatically show the keyboard when opening the search</string>
|
||||||
|
<string name="preference_search_bar_launch_on_enter">Launch on enter</string>
|
||||||
|
<string name="preference_search_bar_launch_on_enter_summary">Launch highlighted match or quick-action upon tapping go</string>
|
||||||
<string name="preference_hidden_items">Hidden search results</string>
|
<string name="preference_hidden_items">Hidden search results</string>
|
||||||
<string name="preference_hidden_items_summary">Manage hidden apps and search results</string>
|
<string name="preference_hidden_items_summary">Manage hidden apps and search results</string>
|
||||||
<string name="preference_screen_tags">Tags</string>
|
<string name="preference_screen_tags">Tags</string>
|
||||||
@ -756,4 +758,4 @@
|
|||||||
<string name="gesture_failed_message">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:</string>
|
<string name="gesture_failed_message">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:</string>
|
||||||
<string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string>
|
<string name="missing_permission_accessibility_gesture_failed">The launcher\'s accessibility service needs to be enabled to perform this action.</string>
|
||||||
<string name="missing_permission_accessibility_gesture_settings">This action requires the launcher\'s accessibility service to be enabled.</string>
|
<string name="missing_permission_accessibility_gesture_settings">This action requires the launcher\'s accessibility service to be enabled.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -222,6 +222,7 @@ message Settings {
|
|||||||
Dark = 2;
|
Dark = 2;
|
||||||
}
|
}
|
||||||
SearchBarColors color = 3;
|
SearchBarColors color = 3;
|
||||||
|
bool launch_on_enter = 4;
|
||||||
}
|
}
|
||||||
SearchBarSettings search_bar = 20;
|
SearchBarSettings search_bar = 20;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user