Search result design adjustments

This commit is contained in:
MM20 2024-05-16 23:01:49 +02:00
parent e41d20331f
commit 8875f6922b
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
30 changed files with 1700 additions and 614 deletions

47
.idea/emulatorDisplays.xml generated Normal file
View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EmulatorDisplays">
<option name="displayStateByAvdFolder">
<map>
<entry key="$USER_HOME$/.android/avd/Pixel_7_API_34.avd">
<value>
<MultiDisplayState>
<option name="displayDescriptors">
<list>
<DisplayDescriptor>
<option name="height" value="2541" />
<option name="width" value="1200" />
</DisplayDescriptor>
<DisplayDescriptor>
<option name="displayId" value="1" />
<option name="height" value="1920" />
<option name="width" value="1080" />
</DisplayDescriptor>
</list>
</option>
<option name="panelState">
<PanelState>
<option name="splitPanel">
<SplitPanelState>
<option name="proportion" value="0.5263158082962036" />
<option name="firstComponent">
<PanelState>
<option name="displayId" value="0" />
</PanelState>
</option>
<option name="secondComponent">
<PanelState>
<option name="displayId" value="1" />
</PanelState>
</option>
</SplitPanelState>
</option>
</PanelState>
</option>
</MultiDisplayState>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@ -43,7 +43,6 @@ import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.logger.Level import org.koin.core.logger.Level
import java.text.Collator
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory { class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
@ -109,12 +108,4 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
.crossfade(200) .crossfade(200)
.build() .build()
} }
companion object {
val collator: Collator by lazy {
Collator.getInstance().apply { strength = Collator.SECONDARY }
}
}
} }

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.assistant
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -25,6 +26,7 @@ import de.mm20.launcher2.ui.launcher.gestures.LauncherGestureHandler
import de.mm20.launcher2.ui.launcher.search.SearchColumn import de.mm20.launcher2.ui.launcher.search.SearchColumn
import de.mm20.launcher2.ui.launcher.search.SearchVM import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -112,6 +114,7 @@ fun AssistantScaffold(
val colorSurface = MaterialTheme.colorScheme.surface val colorSurface = MaterialTheme.colorScheme.surface
val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(darkStatusBarIcons, colorSurface, showStatusBarScrim) { LaunchedEffect(darkStatusBarIcons, colorSurface, showStatusBarScrim) {
if (showStatusBarScrim) { if (showStatusBarScrim) {
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
@ -120,7 +123,7 @@ fun AssistantScaffold(
} else { } else {
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
Color.Transparent, Color.Transparent,
darkIcons = darkStatusBarIcons darkIcons = !isDarkTheme,
) )
} }
} }
@ -167,8 +170,8 @@ fun AssistantScaffold(
SearchColumn( SearchColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
paddingValues = PaddingValues( paddingValues = PaddingValues(
top = (if (bottomSearchBar) 0.dp else 56.dp + webSearchPadding) + 4.dp + windowInsets.calculateTopPadding(), top = (if (bottomSearchBar) 0.dp else 64.dp + webSearchPadding) + 8.dp + windowInsets.calculateTopPadding(),
bottom = (if (bottomSearchBar) 56.dp + webSearchPadding else 0.dp) + 4.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding bottom = (if (bottomSearchBar) 64.dp + webSearchPadding else 0.dp) + 8.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
), ),
reverse = reverseSearchResults, reverse = reverseSearchResults,
state = searchState state = searchState

View File

@ -24,7 +24,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
val selectedTag = MutableStateFlow<String?>(null) val selectedTag = MutableStateFlow<String?>(null)
val showEditButton = settings.showEditButton val showEditButton = settings.showEditButton.stateIn(viewModelScope, SharingStarted.Lazily, false)
abstract val tagsExpanded: Flow<Boolean> abstract val tagsExpanded: Flow<Boolean>
val pinnedTags = favoritesService.getFavorites( val pinnedTags = favoritesService.getFavorites(

View File

@ -80,7 +80,7 @@ fun SearchBar(
when { when {
it == SearchBarLevel.Resting && style != SearchBarStyle.Solid -> 0.dp it == SearchBarLevel.Resting && style != SearchBarStyle.Solid -> 0.dp
it == SearchBarLevel.Raised -> 8.dp it == SearchBarLevel.Raised -> 8.dp
else -> 2.dp else -> 0.dp
} }
} }

View File

@ -0,0 +1,13 @@
package de.mm20.launcher2.ui.ktx
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.ButtonDefaults
import androidx.compose.ui.unit.dp
val ButtonDefaults.TextButtonWithTrailingIconContentPadding
get() = PaddingValues(
start = 16.dp,
top = ContentPadding.calculateTopPadding(),
end = 12.dp,
bottom = ContentPadding.calculateBottomPadding()
)

View File

@ -0,0 +1,138 @@
package de.mm20.launcher2.ui.ktx
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.animateValueAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.shape.CornerBasedShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
/**
* Animates the corners of a shape between its original value and 0.
* For each parameter set to true, the corresponding corner will animate to the original value.
* Otherwise, it will animate to 0.
*/
@Composable
fun CornerBasedShape.animateCorners(
topStart: Boolean,
topEnd: Boolean,
bottomEnd: Boolean,
bottomStart: Boolean,
): CornerBasedShape {
val value by animateShapeAsState(
copy(
topStart = if (topStart) this.topStart else CornerSize(0f),
topEnd = if (topEnd) this.topEnd else CornerSize(0f),
bottomEnd = if (bottomEnd) this.bottomEnd else CornerSize(0f),
bottomStart = if (bottomStart) this.bottomStart else CornerSize(0f)
)
)
return value
}
/**
* Animate between two shapes.
* Limitations:
* - Only works for [RoundedCornerShape] and [CutCornerShape]
* - Shape type should be consistent (e.g. you can't animate between [RoundedCornerShape] and [CutCornerShape]), otherwise the animation will be incorrect
* - Doesn't support percentage based corner sizes
*/
@Composable
fun animateShapeAsState(
shape: CornerBasedShape,
animationSpec: AnimationSpec<CornerBasedShape> = remember { spring() },
visibilityThreshold: CornerBasedShape? = null,
label: String = "ValueAnimation",
finishedListener: ((CornerBasedShape) -> Unit)? = null
): State<CornerBasedShape> {
val density = LocalDensity.current
val converter = remember(shape.javaClass, density) {
if (shape is CutCornerShape) CutCornerShapeConverter(density)
else RoundedCornerShapeConverter(density)
}
return animateValueAsState(
shape,
typeConverter = converter,
animationSpec = animationSpec,
visibilityThreshold = visibilityThreshold,
label = label,
finishedListener = finishedListener
)
}
private class RoundedCornerShapeConverter(
private val density: Density,
) : TwoWayConverter<CornerBasedShape, AnimationVector4D> {
override val convertFromVector: (AnimationVector4D) -> CornerBasedShape
get() = {
RoundedCornerShape(
topStart = it.v1,
topEnd = it.v2,
bottomEnd = it.v3,
bottomStart = it.v4
)
}
override val convertToVector: (CornerBasedShape) -> AnimationVector4D
get() = {
AnimationVector4D(
it.topStart.toPx(Size.Zero, density),
it.topEnd.toPx(Size.Zero, density),
it.bottomEnd.toPx(Size.Zero, density),
it.bottomStart.toPx(Size.Zero, density)
)
}
}
private class CutCornerShapeConverter(
private val density: Density,
) : TwoWayConverter<CornerBasedShape, AnimationVector4D> {
override val convertFromVector: (AnimationVector4D) -> CornerBasedShape
get() = {
CutCornerShape(
topStart = it.v1,
topEnd = it.v2,
bottomEnd = it.v3,
bottomStart = it.v4
)
}
override val convertToVector: (CornerBasedShape) -> AnimationVector4D
get() = {
AnimationVector4D(
it.topStart.toPx(Size.Zero, density),
it.topEnd.toPx(Size.Zero, density),
it.bottomEnd.toPx(Size.Zero, density),
it.bottomStart.toPx(Size.Zero, density)
)
}
}
fun CornerBasedShape.withCorners(
topStart: Boolean = true,
topEnd: Boolean = true,
bottomEnd: Boolean = true,
bottomStart: Boolean = true
): Shape {
if (topStart && topEnd && bottomEnd && bottomStart) return this
if (!topStart && !topEnd && !bottomEnd && !bottomStart) return RectangleShape
return copy(
topStart = if (topStart) this.topStart else CornerSize(0f),
topEnd = if (topEnd) this.topEnd else CornerSize(0f),
bottomEnd = if (bottomEnd) this.bottomEnd else CornerSize(0f),
bottomStart = if (bottomStart) this.bottomStart else CornerSize(0f)
)
}

View File

@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerDefaults import androidx.compose.foundation.pager.PagerDefaults
@ -56,6 +57,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
@ -96,6 +98,7 @@ import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -192,7 +195,8 @@ fun PagerScaffold(
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val colorSurface = MaterialTheme.colorScheme.surface val colorSurface = MaterialTheme.colorScheme.surface
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim) { val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim, isSearchOpen) {
if (isWidgetEditMode) { if (isWidgetEditMode) {
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
colorSurface colorSurface
@ -201,6 +205,11 @@ fun PagerScaffold(
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
colorSurface.copy(0.7f), colorSurface.copy(0.7f),
) )
} else if (isSearchOpen) {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = !isDarkTheme,
)
} else { } else {
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
Color.Transparent, Color.Transparent,
@ -293,8 +302,6 @@ fun PagerScaffold(
handleBackOrHomeEvent() handleBackOrHomeEvent()
} }
val keyboardController = LocalSoftwareKeyboardController.current
val gestureManager = LocalGestureDetector.current val gestureManager = LocalGestureDetector.current
val density = LocalDensity.current val density = LocalDensity.current
@ -395,10 +402,16 @@ fun PagerScaffold(
) { ) {
val minFlingVelocity = 1000.dp.toPixels() val minFlingVelocity = 1000.dp.toPixels()
val colorSurfaceContainer = MaterialTheme.colorScheme.surfaceContainer
HorizontalPager( HorizontalPager(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.drawBehind {
drawRect(
color = colorSurfaceContainer.copy(alpha = -pagerState.getOffsetDistanceInPages(0) * 0.85f),
)
}
.nestedScroll(pagerNestedScrollConnection), .nestedScroll(pagerNestedScrollConnection),
beyondViewportPageCount = 1, beyondViewportPageCount = 1,
reverseLayout = reverse == (LocalLayoutDirection.current == LayoutDirection.Ltr), reverseLayout = reverse == (LocalLayoutDirection.current == LayoutDirection.Ltr),
@ -514,13 +527,13 @@ fun PagerScaffold(
val windowInsets = WindowInsets.safeDrawing.asPaddingValues() val windowInsets = WindowInsets.safeDrawing.asPaddingValues()
val paddingValues = if (bottomSearchBar) { val paddingValues = if (bottomSearchBar) {
PaddingValues( PaddingValues(
top = 4.dp + windowInsets.calculateTopPadding(), top = 8.dp + windowInsets.calculateTopPadding(),
bottom = 60.dp + webSearchPadding + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding bottom = 64.dp + webSearchPadding + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
) )
} else { } else {
PaddingValues( PaddingValues(
bottom = 4.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding, bottom = 8.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding,
top = 60.dp + webSearchPadding + windowInsets.calculateTopPadding() top = 64.dp + webSearchPadding + windowInsets.calculateTopPadding()
) )
} }
SearchColumn( SearchColumn(

View File

@ -10,6 +10,7 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.slideIn import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut import androidx.compose.animation.slideOut
import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -27,6 +28,7 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
@ -50,6 +52,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
@ -80,6 +83,7 @@ import de.mm20.launcher2.ui.launcher.search.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget import de.mm20.launcher2.ui.launcher.widgets.clock.ClockWidget
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -194,7 +198,8 @@ fun PullDownScaffold(
} }
val colorSurface = MaterialTheme.colorScheme.surface val colorSurface = MaterialTheme.colorScheme.surface
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim) { val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface, showStatusBarScrim, isSearchOpen) {
if (isWidgetEditMode) { if (isWidgetEditMode) {
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
colorSurface colorSurface
@ -203,6 +208,11 @@ fun PullDownScaffold(
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
colorSurface.copy(0.7f), colorSurface.copy(0.7f),
) )
} else if (isSearchOpen) {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = !isDarkTheme,
)
} else { } else {
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
Color.Transparent, Color.Transparent,
@ -376,8 +386,14 @@ fun PullDownScaffold(
} }
val insets = WindowInsets.safeDrawing.asPaddingValues() val insets = WindowInsets.safeDrawing.asPaddingValues()
val colorSurfaceContainer = MaterialTheme.colorScheme.surfaceContainer
Box( Box(
modifier = modifier modifier = modifier
.drawBehind {
drawRect(
color = colorSurfaceContainer.copy(alpha = -pagerState.getOffsetDistanceInPages(0) * 0.85f),
)
}
.pointerInput(Unit) { .pointerInput(Unit) {
detectHorizontalDragGestures( detectHorizontalDragGestures(
onDragEnd = { onDragEnd = {
@ -402,7 +418,8 @@ fun PullDownScaffold(
LocalOverscrollConfiguration provides null LocalOverscrollConfiguration provides null
) { ) {
VerticalPager( VerticalPager(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize(),
beyondViewportPageCount = 1, beyondViewportPageCount = 1,
state = pagerState, state = pagerState,
reverseLayout = true, reverseLayout = true,
@ -518,10 +535,10 @@ fun PullDownScaffold(
), ),
), ),
paddingValues = PaddingValues( paddingValues = PaddingValues(
top = windowInsets.calculateTopPadding() + if (!bottomSearchBar) 60.dp + webSearchPadding else 4.dp, top = windowInsets.calculateTopPadding() + if (!bottomSearchBar) 64.dp + webSearchPadding else 8.dp,
bottom = windowInsets.calculateBottomPadding() + bottom = windowInsets.calculateBottomPadding() +
keyboardFilterBarPadding + keyboardFilterBarPadding +
if (bottomSearchBar) 60.dp + webSearchPadding else 4.dp if (bottomSearchBar) 64.dp + webSearchPadding else 8.dp
), ),
state = searchState, state = searchState,
reverse = reverseSearchResults, reverse = reverseSearchResults,
@ -596,7 +613,7 @@ fun PullDownScaffold(
searchBarOffset = { searchBarOffset = {
(if (searchBarFocused || fixedSearchBar) 0 else searchBarOffset.value.toInt() * (if (bottomSearchBar) 1 else -1)) + (if (searchBarFocused || fixedSearchBar) 0 else searchBarOffset.value.toInt() * (if (bottomSearchBar) 1 else -1)) +
with(density) { with(density) {
(editModeSearchBarOffset - if(bottomSearchBar) keyboardFilterBarPadding else 0.dp) (editModeSearchBarOffset - if (bottomSearchBar) keyboardFilterBarPadding else 0.dp)
.toPx() .toPx()
.roundToInt() .roundToInt()
} }

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.search
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -10,27 +11,19 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.Work
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -38,35 +31,42 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.Contact
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R import de.mm20.launcher2.search.Website
import de.mm20.launcher2.ui.common.FavoritesTagSelector
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.LauncherCard import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.PartialLauncherCard import de.mm20.launcher2.ui.component.PartialLauncherCard
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorItem import de.mm20.launcher2.ui.launcher.search.apps.AppResults
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorResults
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarResults
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.contacts.ContactResults
import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavorites
import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavoritesVM import de.mm20.launcher2.ui.launcher.search.favorites.SearchFavoritesVM
import de.mm20.launcher2.ui.launcher.search.files.FileResults
import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters
import de.mm20.launcher2.ui.launcher.search.location.LocationResults
import de.mm20.launcher2.ui.launcher.search.shortcut.ShortcutResults
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterItem
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterResults
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem
import de.mm20.launcher2.ui.launcher.search.website.WebsiteResults
import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItem import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItem
import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleResults
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.locals.LocalCardStyle import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.locals.LocalGridSettings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlin.math.ceil
private const val PRIORITY_MIN = Int.MAX_VALUE
private const val PRIORITY_MAX = Int.MIN_VALUE
@Composable @Composable
fun SearchColumn( fun SearchColumn(
@ -85,8 +85,6 @@ fun SearchColumn(
val favoritesVM: SearchFavoritesVM = viewModel() val favoritesVM: SearchFavoritesVM = viewModel()
val favorites by favoritesVM.favorites.collectAsState(emptyList()) val favorites by favoritesVM.favorites.collectAsState(emptyList())
var showWorkProfileApps by remember { mutableStateOf(false) }
val hideFavs by viewModel.hideFavorites val hideFavs by viewModel.hideFavorites
val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false) val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false)
val apps by viewModel.appResults val apps by viewModel.appResults
@ -101,7 +99,6 @@ fun SearchColumn(
val locations by viewModel.locationResults val locations by viewModel.locationResults
val website by viewModel.websiteResults val website by viewModel.websiteResults
val hiddenResults by viewModel.hiddenResults val hiddenResults by viewModel.hiddenResults
val separateWorkProfile by viewModel.separateWorkProfile.collectAsState(true)
val bestMatch by viewModel.bestMatch val bestMatch by viewModel.bestMatch
@ -119,284 +116,199 @@ fun SearchColumn(
val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false) val favoritesEditButton by favoritesVM.showEditButton.collectAsState(false)
val favoritesTagsExpanded by favoritesVM.tagsExpanded.collectAsState(false) val favoritesTagsExpanded by favoritesVM.tagsExpanded.collectAsState(false)
var showWorkProfileApps by remember { mutableStateOf(false) }
val separateWorkProfile by viewModel.separateWorkProfile.collectAsState(true)
val visibleApps by remember {
derivedStateOf {
when {
!separateWorkProfile -> (apps + workApps).sorted()
workApps.isEmpty() -> apps
apps.isEmpty() -> workApps
showWorkProfileApps -> workApps
else -> apps
}
}
}
var expandedCategory: SearchCategory? by remember(isSearchEmpty) { mutableStateOf(null) }
var selectedContactIndex: Int by remember(contacts) { mutableIntStateOf(-1) }
var selectedFileIndex: Int by remember(files) { mutableIntStateOf(-1) }
var selectedCalendarIndex: Int by remember(events) { mutableIntStateOf(-1) }
var selectedLocationIndex: Int by remember(events) { mutableIntStateOf(-1) }
var selectedShortcutIndex: Int by remember(events) { mutableIntStateOf(-1) }
var selectedArticleIndex: Int by remember(events) { mutableIntStateOf(-1) }
var selectedWebsiteIndex: Int by remember(events) { mutableIntStateOf(-1) }
val showFilters by viewModel.showFilters val showFilters by viewModel.showFilters
AnimatedContent(showFilters) { AnimatedContent(
showFilters,
modifier = modifier.padding(horizontal = 8.dp),
) {
if (it) { if (it) {
BackHandler { BackHandler {
viewModel.showFilters.value = false viewModel.showFilters.value = false
} }
Box( Box(
modifier = modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues),
contentAlignment = if (reverse) Alignment.BottomCenter else Alignment.TopCenter, contentAlignment = if (reverse) Alignment.BottomCenter else Alignment.TopCenter,
) { ) {
LauncherCard( SearchFilters(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp) .padding(top = 4.dp)
) { .background(
SearchFilters( MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
modifier = Modifier MaterialTheme.shapes.medium
.fillMaxWidth() )
.padding(12.dp), .padding(12.dp),
filters = viewModel.filters.value, filters = viewModel.filters.value,
onFiltersChange = { onFiltersChange = {
viewModel.setFilters(it) viewModel.setFilters(it)
} }
) )
}
} }
} else { } else {
LazyColumn( LazyColumn(
state = state, state = state,
modifier = modifier,
userScrollEnabled = userScrollEnabled, userScrollEnabled = userScrollEnabled,
contentPadding = paddingValues, contentPadding = paddingValues,
reverseLayout = reverse, reverseLayout = reverse,
) { ) {
if (!hideFavs && favoritesEnabled) { if (!hideFavs && favoritesEnabled) {
GridResults( SearchFavorites(
items = favorites.toImmutableList(), favorites = favorites,
columns = columns, selectedTag = selectedTag,
key = "favorites", pinnedTags = pinnedTags,
tagsExpanded = favoritesTagsExpanded,
onSelectTag = { favoritesVM.selectTag(it) },
reverse = reverse, reverse = reverse,
before = if (favorites.isEmpty()) { onExpandTags = {
{ favoritesVM.setTagsExpanded(it)
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(
if (selectedTag == null) R.string.favorites_empty else R.string.favorites_empty_tag
),
icon = if (selectedTag == null) Icons.Rounded.Star else Icons.Rounded.Tag,
)
}
} else null,
after = if (pinnedTags.isEmpty() && !favoritesEditButton) {
null
} else {
{
FavoritesTagSelector(
tags = pinnedTags,
selectedTag = selectedTag,
editButton = favoritesEditButton,
reverse = reverse,
onSelectTag = { favoritesVM.selectTag(it) },
scrollState = tagsScrollState,
expanded = favoritesTagsExpanded,
onExpand = { favoritesVM.setTagsExpanded(it) }
)
}
}, },
highlightedItem = bestMatch as? SavableSearchable editButton = favoritesEditButton
) )
} }
GridResults( AppResults(
items = if (separateWorkProfile) if ((showWorkProfileApps || apps.isEmpty()) && workApps.isNotEmpty()) workApps.toImmutableList() else apps.toImmutableList() else listOf( apps = visibleApps,
apps, showTabs = separateWorkProfile && apps.isNotEmpty() && workApps.isNotEmpty(),
workApps highlightedItem = bestMatch as? Application,
).flatten().sorted().toImmutableList(), selectedTab = if (showWorkProfileApps) 1 else 0,
onSelectedTabChange = { showWorkProfileApps = it == 1 },
columns = columns, columns = columns,
reverse = reverse, reverse = reverse
key = "apps",
before = if (separateWorkProfile && workApps.isNotEmpty() && apps.isNotEmpty()) {
{
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.padding(
top = if (reverse) 4.dp else 8.dp,
bottom = if (reverse) 8.dp else 4.dp
),
) {
FilterChip(
modifier = Modifier.padding(horizontal = 8.dp),
selected = !showWorkProfileApps,
onClick = { showWorkProfileApps = false },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Person,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = {
Text(
stringResource(R.string.apps_profile_main),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
FilterChip(
selected = showWorkProfileApps,
onClick = { showWorkProfileApps = true },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Work,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = {
Text(
stringResource(R.string.apps_profile_work),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
} else null,
highlightedItem = bestMatch as? SavableSearchable
) )
ListResults(
before = if (missingShortcutsPermission && !isSearchEmpty) { if (!isSearchEmpty) {
{
MissingPermissionBanner( ShortcutResults(
modifier = Modifier.padding(8.dp), shortcuts = appShortcuts,
text = stringResource( missingPermission = missingShortcutsPermission,
R.string.missing_permission_appshortcuts_search, onPermissionRequest = {
stringResource(R.string.app_name) viewModel.requestAppShortcutPermission(context as AppCompatActivity)
), },
onClick = { viewModel.requestAppShortcutPermission(context as AppCompatActivity) }, onPermissionRequestRejected = {
secondaryAction = { viewModel.disableAppShortcutSearch()
OutlinedButton(onClick = { },
viewModel.disableAppShortcutSearch() reverse = reverse,
}) { selectedIndex = selectedShortcutIndex,
Text( onSelect = { selectedShortcutIndex = it },
stringResource(R.string.turn_off), highlightedItem = bestMatch as? AppShortcut,
) )
}
} UnitConverterResults(
) converters = unitConverter,
reverse = reverse,
truncate = expandedCategory != SearchCategory.UnitConverter,
onShowAll = {
expandedCategory = SearchCategory.UnitConverter
} }
} else null, )
items = appShortcuts.toImmutableList(),
reverse = reverse, CalculatorResults(
key = "shortcuts", calculator,
highlightedItem = bestMatch as? SavableSearchable reverse = reverse
) )
for (conv in unitConverter) {
SingleResult { CalendarResults(
UnitConverterItem(unitConverter = conv) events = events,
} missingPermission = missingCalendarPermission,
onPermissionRequest = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
onPermissionRequestRejected = {
viewModel.disableCalendarSearch()
},
reverse = reverse,
selectedIndex = selectedCalendarIndex,
onSelect = { selectedCalendarIndex = it },
highlightedItem = bestMatch as? CalendarEvent,
)
ContactResults(
contacts = contacts,
missingPermission = missingContactsPermission,
onPermissionRequest = {
viewModel.requestContactsPermission(context as AppCompatActivity)
},
onPermissionRequestRejected = {
viewModel.disableContactsSearch()
},
reverse = reverse,
selectedIndex = selectedContactIndex,
onSelect = { selectedContactIndex = it },
highlightedItem = bestMatch as? Contact,
)
LocationResults(
locations = locations,
missingPermission = missingLocationPermission,
onPermissionRequest = {
viewModel.requestLocationPermission(context as AppCompatActivity)
},
onPermissionRequestRejected = {
viewModel.disableLocationSearch()
},
reverse = reverse,
selectedIndex = selectedLocationIndex,
onSelect = { selectedLocationIndex = it },
highlightedItem = bestMatch as? Location,
)
ArticleResults(
articles = wikipedia,
selectedIndex = selectedArticleIndex,
onSelect = { selectedArticleIndex = it },
highlightedItem = bestMatch as? Article,
reverse = reverse,
)
WebsiteResults(
websites = website,
selectedIndex = selectedWebsiteIndex,
onSelect = { selectedWebsiteIndex = it },
highlightedItem = bestMatch as? Website,
reverse = reverse,
)
FileResults(
files = files,
onPermissionRequest = {
viewModel.requestFilesPermission(context as AppCompatActivity)
},
onPermissionRequestRejected = {
viewModel.disableFilesSearch()
},
reverse = reverse,
highlightedItem = bestMatch as? File,
missingPermission = missingFilesPermission,
selectedIndex = selectedFileIndex,
onSelect = {
selectedFileIndex = it
}
)
} }
for (calc in calculator) {
SingleResult {
CalculatorItem(calculator = calc)
}
}
ListResults(
before = if (missingCalendarPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_calendar_search),
onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableCalendarSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = events.toImmutableList(),
reverse = reverse,
key = "events",
highlightedItem = bestMatch as? SavableSearchable
)
ListResults(
before = if (missingContactsPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_contact_search),
onClick = { viewModel.requestContactsPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableContactsSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = contacts.toImmutableList(),
reverse = reverse,
key = "contacts",
highlightedItem = bestMatch as? SavableSearchable
)
ListResults(
before = if (missingLocationPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_location_search),
onClick = { viewModel.requestLocationPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableLocationSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = locations.toImmutableList(),
reverse = reverse,
key = "locations",
highlightedItem = bestMatch as? SavableSearchable
)
for (wiki in wikipedia) {
SingleResult(highlight = bestMatch == wiki) {
ArticleItem(article = wiki)
}
}
for (ws in website) {
SingleResult(highlight = bestMatch == ws) {
WebsiteItem(website = ws)
}
}
ListResults(
before = if (missingFilesPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_files_search),
onClick = { viewModel.requestFilesPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableFilesSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null,
items = files.toImmutableList(),
reverse = reverse,
key = "files",
highlightedItem = bestMatch as? SavableSearchable
)
} }
} }
@ -411,170 +323,6 @@ fun SearchColumn(
} }
} }
fun LazyListScope.GridResults(
items: ImmutableList<SavableSearchable>,
columns: Int,
reverse: Boolean,
key: String,
before: (@Composable () -> Unit)? = null,
after: (@Composable () -> Unit)? = null,
highlightedItem: SavableSearchable?
) {
if (items.isEmpty() && before == null && after == null) return
if (before != null) {
item(key = "$key-before") {
PartialCardRow(
isFirst = true,
isLast = items.isEmpty() && after == null,
reverse = reverse
) {
before()
}
}
}
val rows = ceil(items.size / columns.toFloat()).toInt()
items(
rows,
key = {
"$key-${items[it * columns].key}"
}
) {
PartialCardRow(
isFirst = it == 0 && before == null,
isLast = it == rows - 1 && after == null,
reverse = reverse
) {
GridRow(
modifier = Modifier.padding(
top = if (if (reverse) it == rows - 1 else it == 0) 4.dp else 0.dp,
bottom = if (if (reverse) it == 0 else it == rows - 1) 2.dp else 0.dp,
),
items = items.subList(
it * columns,
(it * columns + columns).coerceAtMost(items.size)
),
columns = columns,
highlightedItem = highlightedItem
)
}
}
if (after != null) {
item(key = "$key-after") {
PartialCardRow(
isFirst = items.isEmpty() && before == null,
isLast = true,
reverse = reverse
) {
after()
}
}
}
}
@Composable
fun GridRow(
modifier: Modifier = Modifier,
items: ImmutableList<SavableSearchable>,
columns: Int,
showLabels: Boolean = LocalGridSettings.current.showLabels,
highlightedItem: SavableSearchable?
) {
Row(
modifier = modifier
) {
for (item in items) {
GridItem(
modifier = Modifier
.weight(1f)
.padding(4.dp),
item = item,
showLabels = showLabels,
highlight = item.key == highlightedItem?.key
)
}
for (i in 0 until columns - items.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
fun LazyListScope.ListResults(
items: ImmutableList<SavableSearchable>,
reverse: Boolean,
key: String,
before: (@Composable () -> Unit)? = null,
after: (@Composable () -> Unit)? = null,
highlightedItem: SavableSearchable?,
) {
if (before != null) {
item(key = "$key-before") {
PartialCardRow(
isFirst = true,
isLast = items.isEmpty() && after == null,
reverse = reverse
) {
before()
}
}
}
items(
items.size,
key = {
"$key-${items[it].key}"
}
) {
PartialCardRow(
isFirst = it == 0 && before == null,
isLast = it == items.lastIndex && after == null,
reverse = reverse
) {
ListRow(
modifier = Modifier.padding(
top = if (if (reverse) it == items.size - 1 else it == 0) 8.dp else 4.dp,
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
)
}
}
if (after != null) {
item(key = "$key-after") {
PartialCardRow(
isFirst = items.isEmpty() && before == null,
isLast = true,
reverse = reverse
) {
after()
}
}
}
}
@Composable
fun ListRow(
modifier: Modifier = Modifier,
item: SavableSearchable,
highlight: Boolean
) {
Box(
modifier = modifier.padding(
start = 8.dp,
end = 8.dp,
)
) {
ListItem(
modifier = Modifier
.fillMaxWidth(),
item = item,
highlight = highlight
)
}
}
fun LazyListScope.SingleResult( fun LazyListScope.SingleResult(
highlight: Boolean = false, highlight: Boolean = false,
@ -596,31 +344,15 @@ fun LazyListScope.SingleResult(
} }
} }
@Composable enum class SearchCategory {
fun LazyItemScope.PartialCardRow( Apps,
modifier: Modifier = Modifier, Calculator,
isFirst: Boolean, Calendar,
isLast: Boolean, Contacts,
reverse: Boolean, Files,
content: @Composable () -> Unit UnitConverter,
) { Wikipedia,
val isTop = isFirst && !reverse || isLast && reverse Website,
val isBottom = isLast && !reverse || isFirst && reverse Location,
Box( Shortcut,
modifier = modifier
.clipToBounds()
) {
PartialLauncherCard(
modifier = Modifier.padding(
start = 8.dp,
end = 8.dp,
top = if (isTop) 4.dp else 0.dp,
bottom = if (isBottom) 4.dp else 0.dp,
),
isTop = isTop,
isBottom = isBottom,
) {
content()
}
}
} }

View File

@ -0,0 +1,83 @@
package de.mm20.launcher2.ui.launcher.search.apps
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.animateCorners
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
import de.mm20.launcher2.ui.launcher.search.common.grid.GridResults
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalGridSettings
fun LazyListScope.AppResults(
apps: List<Application>,
showTabs: Boolean,
selectedTab: Int,
onSelectedTabChange: (Int) -> Unit,
highlightedItem: Application? = null,
columns: Int,
reverse: Boolean,
) {
GridResults(
key = "app",
items = apps,
itemContent = {
GridItem(
item = it,
showLabels = LocalGridSettings.current.showLabels,
highlight = it.key == highlightedItem?.key
)
},
before = if (showTabs) {
{
Column(
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top,
) {
PrimaryTabRow(
selectedTabIndex = selectedTab,
modifier = Modifier
.fillMaxWidth()
.clip(
MaterialTheme.shapes.medium.animateCorners(
topStart = !reverse,
topEnd = !reverse,
bottomEnd = reverse,
bottomStart = reverse,
)
),
divider = {},
) {
Tab(
selected = selectedTab == 0,
onClick = { onSelectedTabChange(0) },
text = { Text(stringResource(R.string.apps_profile_main)) },
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
Tab(
selected = selectedTab == 1,
onClick = { onSelectedTabChange(1) },
text = { Text(stringResource(R.string.apps_profile_work)) },
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
HorizontalDivider()
}
}
} else null,
reverse = reverse,
columns = columns,
)
}

View File

@ -0,0 +1,22 @@
package de.mm20.launcher2.ui.launcher.search.calculator
import androidx.compose.foundation.lazy.LazyListScope
import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.ui.launcher.search.common.list.ListItemSurface
fun LazyListScope.CalculatorResults(
calculator: List<Calculator>,
reverse: Boolean,
) {
if (calculator.isNotEmpty()) {
item(key = "calculator") {
ListItemSurface(
isFirst = true,
isLast = true,
reverse = reverse,
) {
CalculatorItem(calculator = calculator.first())
}
}
}
}

View File

@ -0,0 +1,59 @@
package de.mm20.launcher2.ui.launcher.search.calendar
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
fun LazyListScope.CalendarResults(
events: List<CalendarEvent>,
missingPermission: Boolean,
onPermissionRequest: () -> Unit,
onPermissionRequestRejected: () -> Unit,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: CalendarEvent?,
reverse: Boolean,
) {
ListResults(
items = events,
key = "calendar",
reverse = reverse,
selectedIndex = selectedIndex,
itemContent = { calendar, showDetails, index ->
ListItem(
modifier = Modifier
.fillMaxWidth(),
item = calendar,
showDetails = showDetails,
onShowDetails = { onSelect(if(it) index else -1) },
highlight = highlightedItem?.key == calendar.key
)
},
before = if (missingPermission) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_calendar_search),
onClick = onPermissionRequest,
secondaryAction = {
OutlinedButton(onClick = onPermissionRequestRejected) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null
)
}

View File

@ -80,6 +80,7 @@ fun GridItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
item: SavableSearchable, item: SavableSearchable,
showLabels: Boolean = true, showLabels: Boolean = true,
labelMaxLines: Int = 1,
highlight: Boolean = false highlight: Boolean = false
) { ) {
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}") val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}")
@ -181,8 +182,9 @@ fun GridItem(
text = item.labelOverride ?: item.label, text = item.labelOverride ?: item.label,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
maxLines = 1, maxLines = labelMaxLines,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onBackground,
) )
} }
@ -223,7 +225,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
.fillMaxSize() .fillMaxSize()
.systemBarsPadding() .systemBarsPadding()
.imePadding() .imePadding()
.padding(horizontal = 16.dp) .padding(horizontal = 8.dp)
.then( .then(
if (show.targetState) { if (show.targetState) {
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
@ -242,7 +244,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
modifier = Modifier modifier = Modifier
.placeOverlay( .placeOverlay(
origin.translate( origin.translate(
-16.dp.toPixels(), -8.dp.toPixels(),
-WindowInsets.systemBars.union(WindowInsets.ime) -WindowInsets.systemBars.union(WindowInsets.ime)
.getTop(LocalDensity.current).toFloat() .getTop(LocalDensity.current).toFloat()
), ),

View File

@ -0,0 +1,130 @@
package de.mm20.launcher2.ui.launcher.search.common.grid
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.ktx.withCorners
import de.mm20.launcher2.ui.locals.LocalCardStyle
import kotlin.math.ceil
fun <T : SavableSearchable> LazyListScope.GridResults(
key: String,
items: List<T>,
itemContent: @Composable (T) -> Unit,
before: @Composable (() -> Unit)? = null,
after: @Composable (() -> Unit)? = null,
columns: Int,
reverse: Boolean = false,
) {
if (before != null) {
item(
key = "$key-before",
) {
val isTop = !reverse || items.isEmpty() && after == null
val isBottom = reverse || items.isEmpty() && after == null
Box(
modifier = Modifier
.background(
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
MaterialTheme.shapes.medium.withCorners(
topStart = isTop,
topEnd = isTop,
bottomEnd = isBottom,
bottomStart = isBottom,
)
)
) {
before()
}
}
}
val rows = ceil(items.size / columns.toFloat()).toInt()
items(
rows,
key = {
"$key-$it"
}
) {
val isFirst = it == 0 && before == null
val isLast = it == rows - 1 && after == null
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
top = if (reverse && isLast) 8.dp else 0.dp,
bottom = if (!reverse && isLast) 8.dp else 0.dp,
)
.background(
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
MaterialTheme.shapes.medium.withCorners(
topStart = isFirst && !reverse || isLast && reverse,
topEnd = isFirst && !reverse || isLast && reverse,
bottomEnd = isLast && !reverse || isFirst && reverse,
bottomStart = isLast && !reverse || isFirst && reverse,
)
)
.padding(
top = if (it == 0) 8.dp else 0.dp,
bottom = if (it == rows - 1) 8.dp else 0.dp,
start = 4.dp,
end = 4.dp,
)
) {
Row {
for (i in 0 until columns) {
val item = items.getOrNull(it * columns + i)
if (item != null) {
Box(
modifier = Modifier
.weight(1f)
.padding(4.dp)
) {
itemContent(item)
}
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
}
if (after != null) {
item(
key = "$key-after",
) {
val isTop = reverse || items.isEmpty() && before == null
val isBottom = !reverse || items.isEmpty() && before == null
Box(
modifier = Modifier
.padding(
top = if (reverse) 8.dp else 0.dp,
bottom = if (!reverse) 8.dp else 0.dp,
)
.background(
MaterialTheme.colorScheme.surface.copy(alpha = LocalCardStyle.current.opacity),
MaterialTheme.shapes.medium.withCorners(
topStart = isTop,
topEnd = isTop,
bottomEnd = isBottom,
bottomStart = isBottom,
)
)
) {
after()
}
}
}
}

View File

@ -1,8 +1,21 @@
package de.mm20.launcher2.ui.launcher.search.common.list package de.mm20.launcher2.ui.launcher.search.common.list
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.* import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
@ -10,12 +23,13 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.AppShortcut import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.CalendarEvent import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.Contact import de.mm20.launcher2.search.Contact
import de.mm20.launcher2.search.File import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.Location import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.component.InnerCard import de.mm20.launcher2.search.Website
import de.mm20.launcher2.ui.ktx.toPixels import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
@ -24,15 +38,18 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItem
import de.mm20.launcher2.ui.launcher.search.listItemViewModel import de.mm20.launcher2.ui.launcher.search.listItemViewModel
import de.mm20.launcher2.ui.launcher.search.location.LocationItem import de.mm20.launcher2.ui.launcher.search.location.LocationItem
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
import de.mm20.launcher2.ui.launcher.search.website.WebsiteItem
import de.mm20.launcher2.ui.launcher.search.wikipedia.ArticleItem
import de.mm20.launcher2.ui.locals.LocalGridSettings import de.mm20.launcher2.ui.locals.LocalGridSettings
@Composable @Composable
fun ListItem( fun ListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
item: SavableSearchable, item: SavableSearchable,
highlight: Boolean = false highlight: Boolean = false,
showDetails: Boolean,
onShowDetails: (Boolean) -> Unit
) { ) {
var showDetails by remember { mutableStateOf(false) }
val context = LocalContext.current val context = LocalContext.current
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}") val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}")
@ -41,104 +58,147 @@ fun ListItem(
LaunchedEffect(item, iconSize) { LaunchedEffect(item, iconSize) {
viewModel.init(item, iconSize.toInt()) viewModel.init(item, iconSize.toInt())
} }
LaunchedEffect(showDetails) { LaunchedEffect(showDetails) {
if (showDetails) viewModel.requestUpdatedSearchable(context) if (showDetails) viewModel.requestUpdatedSearchable(context)
} }
val background by animateColorAsState(
if (highlight) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface.copy(
alpha = 0f
)
)
val item = viewModel.searchable.collectAsState().value ?: item val item = viewModel.searchable.collectAsState().value ?: item
var bounds by remember { mutableStateOf(Rect.Zero) } var bounds by remember { mutableStateOf(Rect.Zero) }
InnerCard( Box(
modifier = modifier modifier = modifier
.background(background)
.onGloballyPositioned { .onGloballyPositioned {
bounds = it.boundsInWindow() bounds = it.boundsInWindow()
}, },
highlight = highlight,
raised = showDetails
) { ) {
when (item) { CompositionLocalProvider(
is Contact -> { LocalContentColor provides MaterialTheme.colorScheme.onSurface
ContactItem( ) {
modifier = Modifier when (item) {
.fillMaxWidth() is Contact -> {
.combinedClickable( ContactItem(
enabled = !showDetails, modifier = Modifier
onClick = { showDetails = true }, .fillMaxWidth()
onLongClick = { showDetails = true } .combinedClickable(
), enabled = !showDetails,
contact = item, onClick = { onShowDetails(true) },
showDetails = showDetails, onLongClick = { onShowDetails(true) }
onBack = { showDetails = false } ),
) contact = item,
showDetails = showDetails,
onBack = { onShowDetails(false) }
)
}
is File -> {
FileItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = {
if (!viewModel.launch(context, bounds)) {
onShowDetails(true)
}
},
onLongClick = { onShowDetails(true) }
),
file = item,
showDetails = showDetails,
onBack = { onShowDetails(false) }
)
}
is CalendarEvent -> {
CalendarItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = { onShowDetails(true) },
onLongClick = { onShowDetails(true) }
)
.padding(top = 4.dp, end = 4.dp, bottom = 4.dp),
calendar = item,
showDetails = showDetails,
onBack = { onShowDetails(false) }
)
}
is Location -> {
LocationItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = { onShowDetails(true) },
onLongClick = { onShowDetails(true) }),
location = item,
showDetails = showDetails,
onBack = { onShowDetails(false) }
)
}
is AppShortcut -> {
AppShortcutItem(
shortcut = item,
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = {
if (!viewModel.launch(context, bounds)) {
onShowDetails(true)
}
},
onLongClick = { onShowDetails(true) }
),
showDetails = showDetails,
onBack = { onShowDetails(false) }
)
}
is Article -> {
ArticleItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = {
if (!viewModel.launch(context, bounds)) {
onShowDetails(true)
}
},
onLongClick = { onShowDetails(true) }),
article = item,
)
}
is Website -> {
WebsiteItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = {
if (!viewModel.launch(context, bounds)) {
onShowDetails(true)
}
},
onLongClick = { onShowDetails(true) }),
website = item,
)
}
} }
is File -> {
FileItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = {
if (!viewModel.launch(context, bounds)) {
showDetails = true
}
},
onLongClick = { showDetails = true }
),
file = item,
showDetails = showDetails,
onBack = { showDetails = false }
)
}
is CalendarEvent -> {
CalendarItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = { showDetails = true },
onLongClick = { showDetails = true }
),
calendar = item,
showDetails = showDetails,
onBack = { showDetails = false }
)
}
is Location -> {
LocationItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = { showDetails = true },
onLongClick = { showDetails = true }),
location = item,
showDetails = showDetails,
onBack = { showDetails = false }
)
}
is AppShortcut -> {
AppShortcutItem(
shortcut = item,
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = {
if (!viewModel.launch(context, bounds)) {
showDetails = true
}
},
onLongClick = { showDetails = true }
),
showDetails = showDetails,
onBack = { showDetails = false }
)
}
} }
} }
} }

View File

@ -0,0 +1,159 @@
package de.mm20.launcher2.ui.launcher.search.common.list
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.ktx.animateCorners
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalCardStyle
fun <T : SavableSearchable> LazyListScope.ListResults(
key: String,
items: List<T>,
itemContent: @Composable LazyItemScope.(T, Boolean, Int) -> Unit,
before: @Composable (LazyItemScope.() -> Unit)? = null,
after: @Composable (LazyItemScope.() -> Unit)? = null,
reverse: Boolean = false,
selectedIndex: Int = -1,
) {
if (before != null) {
item(
key = "$key-before",
) {
ListItemSurface(
isFirst = true,
isLast = after == null && items.isEmpty(),
reverse = reverse,
isBeforeExpanded = selectedIndex == 0,
) {
before()
}
}
}
val rows = items.size
items(
items.size,
key = {
"$key-${items[it].key}"
},
) {
val item = items[it]
val showDetails = it == selectedIndex
ListItemSurface(
isFirst = it == 0 && before == null,
isLast = it == rows - 1 && after == null,
reverse = reverse,
isExpanded = showDetails,
isBeforeExpanded = selectedIndex - 1 == it,
isAfterExpanded = selectedIndex + 1 == it,
) {
itemContent(item, showDetails, it)
}
}
if (after != null) {
item(
key = "$key-after",
) {
ListItemSurface(
isFirst = before == null && items.isEmpty(),
isLast = true,
reverse = reverse,
isAfterExpanded = selectedIndex == items.lastIndex,
) {
after()
}
}
}
}
@Composable
fun LazyItemScope.ListItemSurface(
isFirst: Boolean = false,
isLast: Boolean = false,
reverse: Boolean = false,
isExpanded: Boolean = false,
isBeforeExpanded: Boolean = false,
isAfterExpanded: Boolean = false,
content: @Composable ColumnScope.() -> Unit,
) {
val transition = updateTransition(isExpanded)
val elevation by transition.animateDp {
if (it) 2.dp else 0.dp
}
val backgroundAlpha by transition.animateFloat {
if (it) 1f else LocalCardStyle.current.opacity
}
val padding by transition.animateDp {
if (it) 8.dp else 0.dp
}
val modifier = if (reverse) {
Modifier
.animateItem()
.padding(
bottom = if (!isFirst) padding else 0.dp,
top = if (!isLast) padding else 8.dp
)
.shadow(
elevation = elevation,
MaterialTheme.shapes.medium.animateCorners(
bottomStart = isFirst || isExpanded || isAfterExpanded,
bottomEnd = isFirst || isExpanded || isAfterExpanded,
topEnd = isLast || isExpanded || isBeforeExpanded,
topStart = isLast || isExpanded || isBeforeExpanded,
),
true,
)
} else {
Modifier
.padding(
top = if (!isFirst) padding else 0.dp,
bottom = if (!isLast) padding else 8.dp
)
.shadow(
elevation = elevation,
MaterialTheme.shapes.medium.animateCorners(
topStart = isFirst || isExpanded || isAfterExpanded,
topEnd = isFirst || isExpanded || isAfterExpanded,
bottomEnd = isLast || isExpanded || isBeforeExpanded,
bottomStart = isLast || isExpanded || isBeforeExpanded,
),
true,
)
}
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface.copy(backgroundAlpha)),
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
) {
AnimatedVisibility(!isFirst && !isExpanded && !isAfterExpanded) {
HorizontalDivider()
}
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
content()
}
}
}

View File

@ -1,12 +1,21 @@
package de.mm20.launcher2.ui.launcher.search.common.list package de.mm20.launcher2.ui.launcher.search.common.list
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.layout.BottomReversed import de.mm20.launcher2.ui.layout.BottomReversed
@ -18,18 +27,25 @@ fun SearchResultList(
reverse: Boolean = false, reverse: Boolean = false,
highlightedItem: SavableSearchable? = null highlightedItem: SavableSearchable? = null
) { ) {
var selectedIndex by remember { mutableIntStateOf(-1) }
Column( Column(
modifier = modifier, modifier = modifier
.clip(MaterialTheme.shapes.small)
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small),
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top
) { ) {
for (item in items) { for ((i, item) in items.withIndex()) {
key(item.key) { key(item.key) {
if (i != 0) {
HorizontalDivider()
}
ListItem( ListItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.padding(4.dp),
item = item, item = item,
highlight = item.key == highlightedItem?.key highlight = item.key == highlightedItem?.key,
showDetails = selectedIndex == i,
onShowDetails = { selectedIndex = if (it) i else -1}
) )
} }
} }

View File

@ -104,9 +104,7 @@ fun ContactItem(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
val icon by viewModel.icon.collectAsStateWithLifecycle() val icon by viewModel.icon.collectAsStateWithLifecycle()
val padding by transition.animateDp(label = "iconPadding") { val padding = 16.dp
if (it) 16.dp else 8.dp
}
ShapedLauncherIcon( ShapedLauncherIcon(
size = 48.dp, size = 48.dp,
modifier = Modifier modifier = Modifier

View File

@ -0,0 +1,59 @@
package de.mm20.launcher2.ui.launcher.search.contacts
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.Contact
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
fun LazyListScope.ContactResults(
contacts: List<Contact>,
missingPermission: Boolean,
onPermissionRequest: () -> Unit,
onPermissionRequestRejected: () -> Unit,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: Contact?,
reverse: Boolean,
) {
ListResults(
items = contacts,
key = "contact",
reverse = reverse,
selectedIndex = selectedIndex,
itemContent = { contact, showDetails, index ->
ListItem(
modifier = Modifier
.fillMaxWidth(),
item = contact,
showDetails = showDetails,
onShowDetails = { onSelect(if(it) index else -1) },
highlight = highlightedItem?.key == contact.key
)
},
before = if (missingPermission) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_contact_search),
onClick = onPermissionRequest,
secondaryAction = {
OutlinedButton(onClick = onPermissionRequestRejected) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null
)
}

View File

@ -0,0 +1,77 @@
package de.mm20.launcher2.ui.launcher.search.favorites
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.FavoritesTagSelector
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.ui.launcher.widgets.favorites.FavoritesWidgetVM
fun LazyListScope.SearchFavorites(
favorites: List<SavableSearchable>,
pinnedTags: List<Tag>,
selectedTag: String?,
tagsExpanded: Boolean,
onExpandTags: (Boolean) -> Unit,
onSelectTag: (String?) -> Unit,
editButton: Boolean,
reverse: Boolean,
) {
item(
key = "favorites",
) {
Column(
modifier = Modifier
.padding(
top = if (reverse) 8.dp else 0.dp,
bottom = if (reverse) 0.dp else 8.dp,
)
.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.medium)
) {
if (favorites.isNotEmpty()) {
SearchResultGrid(favorites)
} else {
Banner(
modifier = Modifier.padding(16.dp),
text = stringResource(
if (selectedTag == null) R.string.favorites_empty else R.string.favorites_empty_tag
),
icon = if (selectedTag == null) Icons.Rounded.Star else Icons.Rounded.Tag,
)
}
if (pinnedTags.isNotEmpty() || editButton) {
FavoritesTagSelector(
tags = pinnedTags,
selectedTag = selectedTag,
editButton = editButton,
reverse = false,
onSelectTag = onSelectTag,
scrollState = rememberScrollState(),
expanded = tagsExpanded,
onExpand = onExpandTags,
)
}
}
}
}

View File

@ -0,0 +1,59 @@
package de.mm20.launcher2.ui.launcher.search.files
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.File
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
fun LazyListScope.FileResults(
files: List<File>,
missingPermission: Boolean,
onPermissionRequest: () -> Unit,
onPermissionRequestRejected: () -> Unit,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: File? = null,
reverse: Boolean,
) {
ListResults(
items = files,
key = "file",
reverse = reverse,
selectedIndex = selectedIndex,
itemContent = { file, showDetails, index ->
ListItem(
modifier = Modifier
.fillMaxWidth(),
item = file,
showDetails = showDetails,
onShowDetails = { onSelect(if (it) index else -1) },
highlight = highlightedItem?.key == file.key
)
},
before = if (missingPermission) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_files_search),
onClick = onPermissionRequest,
secondaryAction = {
OutlinedButton(onClick = onPermissionRequestRejected) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null
)
}

View File

@ -156,7 +156,7 @@ fun LocationItem(
enter = slideIn { IntOffset(-it.width, 0) } + fadeIn(), enter = slideIn { IntOffset(-it.width, 0) } + fadeIn(),
exit = slideOut { IntOffset(-it.width, 0) } + fadeOut(), exit = slideOut { IntOffset(-it.width, 0) } + fadeOut(),
) )
.padding(8.dp), .padding(12.dp),
size = 48.dp, size = 48.dp,
icon = { icon }, icon = { icon },
badge = { badge }, badge = { badge },
@ -201,7 +201,7 @@ fun LocationItem(
} }
Compass( Compass(
targetHeading = targetHeading, targetHeading = targetHeading,
modifier = Modifier.padding(end = 8.dp) then modifier = Modifier.padding(end = 12.dp) then
if (!showMap) { if (!showMap) {
Modifier.sharedBounds( Modifier.sharedBounds(
rememberSharedContentState("compass"), rememberSharedContentState("compass"),

View File

@ -0,0 +1,59 @@
package de.mm20.launcher2.ui.launcher.search.location
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.Location
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
fun LazyListScope.LocationResults(
locations: List<Location>,
missingPermission: Boolean,
onPermissionRequest: () -> Unit,
onPermissionRequestRejected: () -> Unit,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: Location?,
reverse: Boolean,
) {
ListResults(
items = locations,
key = "location",
reverse = reverse,
selectedIndex = selectedIndex,
itemContent = { location, showDetails, index ->
ListItem(
modifier = Modifier
.fillMaxWidth(),
item = location,
showDetails = showDetails,
onShowDetails = { onSelect(if(it) index else -1) },
highlight = highlightedItem?.key == location.key
)
},
before = if (missingPermission) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_location_search),
onClick = onPermissionRequest,
secondaryAction = {
OutlinedButton(onClick = onPermissionRequestRejected) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null
)
}

View File

@ -0,0 +1,59 @@
package de.mm20.launcher2.ui.launcher.search.shortcut
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
fun LazyListScope.ShortcutResults(
shortcuts: List<AppShortcut>,
missingPermission: Boolean,
onPermissionRequest: () -> Unit,
onPermissionRequestRejected: () -> Unit,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: AppShortcut?,
reverse: Boolean,
) {
ListResults(
items = shortcuts,
key = "shortcut",
reverse = reverse,
selectedIndex = selectedIndex,
itemContent = { shortcut, showDetails, index ->
ListItem(
modifier = Modifier
.fillMaxWidth(),
item = shortcut,
showDetails = showDetails,
onShowDetails = { onSelect(if(it) index else -1) },
highlight = highlightedItem?.key == shortcut.key
)
},
before = if (missingPermission) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.missing_permission_appshortcuts_search),
onClick = onPermissionRequest,
secondaryAction = {
OutlinedButton(onClick = onPermissionRequestRejected) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
}
} else null
)
}

View File

@ -140,21 +140,3 @@ fun UnitConverterItem(
} }
} }
} }
fun getDimensionIcon(dimension: Dimension): ImageVector {
return when (dimension) {
Dimension.Mass -> Icons.Rounded.FitnessCenter
Dimension.Length -> Icons.Rounded.Straighten
Dimension.Velocity -> Icons.Rounded.Speed
Dimension.Volume -> TODO()
Dimension.Area -> Icons.Rounded.SquareFoot
Dimension.Currency -> Icons.Rounded.Toll
Dimension.Data -> Icons.Rounded.Storage
Dimension.Bitrate -> TODO()
Dimension.Pressure -> TODO()
Dimension.Energy -> Icons.Rounded.Bolt
Dimension.Frequency -> TODO()
Dimension.Temperature -> Icons.Rounded.Thermostat
Dimension.Time -> Icons.Rounded.Schedule
}
}

View File

@ -0,0 +1,252 @@
package de.mm20.launcher2.ui.launcher.search.unitconverter
import android.icu.text.DateFormat
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Bolt
import androidx.compose.material.icons.rounded.FitnessCenter
import androidx.compose.material.icons.rounded.Schedule
import androidx.compose.material.icons.rounded.Speed
import androidx.compose.material.icons.rounded.SquareFoot
import androidx.compose.material.icons.rounded.Storage
import androidx.compose.material.icons.rounded.Straighten
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.Toll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.CurrencyUnitConverter
import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.ktx.TextButtonWithTrailingIconContentPadding
import de.mm20.launcher2.ui.launcher.search.common.list.ListItemSurface
import de.mm20.launcher2.unitconverter.Dimension
import java.util.Date
import kotlin.math.min
fun LazyListScope.UnitConverterResults(
converters: List<UnitConverter>,
truncate: Boolean,
onShowAll: () -> Unit,
reverse: Boolean,
) {
if (converters.isNotEmpty()) {
val converter = converters.first()
item(
key = "converter-header",
) {
ListItemSurface(
isFirst = true,
reverse = reverse,
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
) {
Text(
text = converter.inputValue.let { "${it.formattedValue} ${it.formattedName}" },
style = MaterialTheme.typography.titleLarge,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
if (converter is CurrencyUnitConverter) {
var showDisclaimer by remember { mutableStateOf(false) }
val df = DateFormat.getDateInstance(DateFormat.SHORT)
Row(
modifier = Modifier
.padding(top = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "${df.format(Date(converter.updateTimestamp))}",
style = MaterialTheme.typography.labelSmall,
)
Text(
text = stringResource(id = R.string.disclaimer),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.clickable {
showDisclaimer = true
}
)
}
if (showDisclaimer) {
AlertDialog(
onDismissRequest = { showDisclaimer = false },
confirmButton = {
TextButton(onClick = { showDisclaimer = false }) {
Text(text = stringResource(id = R.string.close))
}
},
title = { Text(stringResource(id = R.string.disclaimer)) },
text = {
Text(
stringResource(
id = R.string.disclaimer_currency_converter,
df.format(Date(converter.updateTimestamp))
)
)
}
)
}
}
}
Icon(
imageVector = getDimensionIcon(converter.dimension),
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.padding(
top = 20.dp,
bottom = 20.dp,
start = 16.dp,
end = 18.dp
)
.size(24.dp)
)
}
}
}
val count = if (truncate) min(5, converter.values.size) else converter.values.size
items(
count,
key = { "converter-${converter.values[it].symbol}" }
) {
val value = converter.values[it]
ListItemSurface(
isLast = it == converter.values.lastIndex,
reverse = reverse,
) {
Column {
val clipboardManager = LocalClipboardManager.current
Row(
modifier = Modifier
.clickable {
clipboardManager.setText(buildAnnotatedString {
append(value.value.toString())
append(" ")
append(value.symbol)
})
}
.padding(
start = 16.dp,
end = 12.dp,
top = 12.dp,
bottom = 12.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.widthIn(min = 48.dp),
text = value.formattedValue,
style = MaterialTheme.typography.labelLarge
)
Text(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
text = value.formattedName,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Box(
modifier = Modifier
.background(
MaterialTheme.colorScheme.secondaryContainer,
MaterialTheme.shapes.extraSmall,
)
.height(36.dp)
.widthIn(min = 36.dp)
.padding(4.dp),
contentAlignment = Alignment.Center,
) {
Text(
textAlign = TextAlign.Center,
text = value.symbol,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSecondaryContainer,
maxLines = 1,
)
}
}
}
}
}
if (truncate && converter.values.size > 5) {
item(
key = "converter-footer"
) {
ListItemSurface(
isLast = true,
reverse = reverse,
) {
TextButton(
modifier = Modifier
.align(Alignment.End)
.padding(4.dp),
onClick = onShowAll,
contentPadding = ButtonDefaults.TextButtonWithTrailingIconContentPadding,
) {
Text(stringResource(R.string.unit_converter_show_all))
Icon(
Icons.AutoMirrored.Rounded.ArrowForward,
null,
modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
}
}
}
}
}
}
fun getDimensionIcon(dimension: Dimension): ImageVector {
return when (dimension) {
Dimension.Mass -> Icons.Rounded.FitnessCenter
Dimension.Length -> Icons.Rounded.Straighten
Dimension.Velocity -> Icons.Rounded.Speed
Dimension.Volume -> TODO()
Dimension.Area -> Icons.Rounded.SquareFoot
Dimension.Currency -> Icons.Rounded.Toll
Dimension.Data -> Icons.Rounded.Storage
Dimension.Bitrate -> TODO()
Dimension.Pressure -> TODO()
Dimension.Energy -> Icons.Rounded.Bolt
Dimension.Frequency -> TODO()
Dimension.Temperature -> Icons.Rounded.Thermostat
Dimension.Time -> Icons.Rounded.Schedule
}
}

View File

@ -0,0 +1,29 @@
package de.mm20.launcher2.ui.launcher.search.website
import androidx.compose.foundation.lazy.LazyListScope
import de.mm20.launcher2.search.Website
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
fun LazyListScope.WebsiteResults(
websites: List<Website>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: Website?,
reverse: Boolean,
) {
ListResults(
key = "website",
items = websites,
itemContent = { website, showDetails, index ->
ListItem(
item = website,
showDetails = showDetails,
onShowDetails = { onSelect(if(it) index else -1) },
highlight = website.key == highlightedItem?.key,
)
},
selectedIndex = selectedIndex,
reverse = reverse,
)
}

View File

@ -61,9 +61,7 @@ fun ArticleItem(
} }
Column( Column(
modifier = modifier.clickable { modifier = modifier
viewModel.launch(context)
}
) { ) {
if (!article.imageUrl.isNullOrEmpty()) { if (!article.imageUrl.isNullOrEmpty()) {
AsyncImage( AsyncImage(

View File

@ -0,0 +1,29 @@
package de.mm20.launcher2.ui.launcher.search.wikipedia
import androidx.compose.foundation.lazy.LazyListScope
import de.mm20.launcher2.search.Article
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
fun LazyListScope.ArticleResults(
articles: List<Article>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: Article?,
reverse: Boolean,
) {
ListResults(
key = "article",
items = articles,
itemContent = { article, showDetails, index ->
ListItem(
item = article,
showDetails = showDetails,
onShowDetails = { onSelect(if(it) index else -1) },
highlight = article.key == highlightedItem?.key,
)
},
selectedIndex = selectedIndex,
reverse = reverse,
)
}