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.core.context.startKoin
import org.koin.core.logger.Level
import java.text.Collator
import kotlin.coroutines.CoroutineContext
class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
@ -109,12 +108,4 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
.crossfade(200)
.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.foundation.layout.*
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.MaterialTheme
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.SearchVM
import de.mm20.launcher2.ui.launcher.searchbar.LauncherSearchBar
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
@ -112,6 +114,7 @@ fun AssistantScaffold(
val colorSurface = MaterialTheme.colorScheme.surface
val isDarkTheme = LocalDarkTheme.current
LaunchedEffect(darkStatusBarIcons, colorSurface, showStatusBarScrim) {
if (showStatusBarScrim) {
systemUiController.setStatusBarColor(
@ -120,7 +123,7 @@ fun AssistantScaffold(
} else {
systemUiController.setStatusBarColor(
Color.Transparent,
darkIcons = darkStatusBarIcons
darkIcons = !isDarkTheme,
)
}
}
@ -167,8 +170,8 @@ fun AssistantScaffold(
SearchColumn(
modifier = Modifier.fillMaxSize(),
paddingValues = PaddingValues(
top = (if (bottomSearchBar) 0.dp else 56.dp + webSearchPadding) + 4.dp + windowInsets.calculateTopPadding(),
bottom = (if (bottomSearchBar) 56.dp + webSearchPadding else 0.dp) + 4.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
top = (if (bottomSearchBar) 0.dp else 64.dp + webSearchPadding) + 8.dp + windowInsets.calculateTopPadding(),
bottom = (if (bottomSearchBar) 64.dp + webSearchPadding else 0.dp) + 8.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
),
reverse = reverseSearchResults,
state = searchState

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.search
import androidx.activity.compose.BackHandler
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
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.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -38,35 +31,42 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
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.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.ui.R
import de.mm20.launcher2.ui.common.FavoritesTagSelector
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.search.Website
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.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.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.files.FileResults
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.UnitConverterResults
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.ArticleResults
import de.mm20.launcher2.ui.launcher.sheets.HiddenItemsSheet
import de.mm20.launcher2.ui.launcher.sheets.LocalBottomSheetManager
import de.mm20.launcher2.ui.locals.LocalCardStyle
import de.mm20.launcher2.ui.locals.LocalGridSettings
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
fun SearchColumn(
@ -85,8 +85,6 @@ fun SearchColumn(
val favoritesVM: SearchFavoritesVM = viewModel()
val favorites by favoritesVM.favorites.collectAsState(emptyList())
var showWorkProfileApps by remember { mutableStateOf(false) }
val hideFavs by viewModel.hideFavorites
val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false)
val apps by viewModel.appResults
@ -101,7 +99,6 @@ fun SearchColumn(
val locations by viewModel.locationResults
val website by viewModel.websiteResults
val hiddenResults by viewModel.hiddenResults
val separateWorkProfile by viewModel.separateWorkProfile.collectAsState(true)
val bestMatch by viewModel.bestMatch
@ -119,284 +116,199 @@ fun SearchColumn(
val favoritesEditButton by favoritesVM.showEditButton.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
AnimatedContent(showFilters) {
AnimatedContent(
showFilters,
modifier = modifier.padding(horizontal = 8.dp),
) {
if (it) {
BackHandler {
viewModel.showFilters.value = false
}
Box(
modifier = modifier
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = if (reverse) Alignment.BottomCenter else Alignment.TopCenter,
) {
LauncherCard(
SearchFilters(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
SearchFilters(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
filters = viewModel.filters.value,
onFiltersChange = {
viewModel.setFilters(it)
}
)
}
.padding(top = 4.dp)
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
MaterialTheme.shapes.medium
)
.padding(12.dp),
filters = viewModel.filters.value,
onFiltersChange = {
viewModel.setFilters(it)
}
)
}
} else {
LazyColumn(
state = state,
modifier = modifier,
userScrollEnabled = userScrollEnabled,
contentPadding = paddingValues,
reverseLayout = reverse,
) {
if (!hideFavs && favoritesEnabled) {
GridResults(
items = favorites.toImmutableList(),
columns = columns,
key = "favorites",
SearchFavorites(
favorites = favorites,
selectedTag = selectedTag,
pinnedTags = pinnedTags,
tagsExpanded = favoritesTagsExpanded,
onSelectTag = { favoritesVM.selectTag(it) },
reverse = reverse,
before = if (favorites.isEmpty()) {
{
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) }
)
}
onExpandTags = {
favoritesVM.setTagsExpanded(it)
},
highlightedItem = bestMatch as? SavableSearchable
editButton = favoritesEditButton
)
}
GridResults(
items = if (separateWorkProfile) if ((showWorkProfileApps || apps.isEmpty()) && workApps.isNotEmpty()) workApps.toImmutableList() else apps.toImmutableList() else listOf(
apps,
workApps
).flatten().sorted().toImmutableList(),
AppResults(
apps = visibleApps,
showTabs = separateWorkProfile && apps.isNotEmpty() && workApps.isNotEmpty(),
highlightedItem = bestMatch as? Application,
selectedTab = if (showWorkProfileApps) 1 else 0,
onSelectedTabChange = { showWorkProfileApps = it == 1 },
columns = columns,
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
reverse = reverse
)
ListResults(
before = if (missingShortcutsPermission && !isSearchEmpty) {
{
MissingPermissionBanner(
modifier = Modifier.padding(8.dp),
text = stringResource(
R.string.missing_permission_appshortcuts_search,
stringResource(R.string.app_name)
),
onClick = { viewModel.requestAppShortcutPermission(context as AppCompatActivity) },
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableAppShortcutSearch()
}) {
Text(
stringResource(R.string.turn_off),
)
}
}
)
if (!isSearchEmpty) {
ShortcutResults(
shortcuts = appShortcuts,
missingPermission = missingShortcutsPermission,
onPermissionRequest = {
viewModel.requestAppShortcutPermission(context as AppCompatActivity)
},
onPermissionRequestRejected = {
viewModel.disableAppShortcutSearch()
},
reverse = reverse,
selectedIndex = selectedShortcutIndex,
onSelect = { selectedShortcutIndex = it },
highlightedItem = bestMatch as? AppShortcut,
)
UnitConverterResults(
converters = unitConverter,
reverse = reverse,
truncate = expandedCategory != SearchCategory.UnitConverter,
onShowAll = {
expandedCategory = SearchCategory.UnitConverter
}
} else null,
items = appShortcuts.toImmutableList(),
reverse = reverse,
key = "shortcuts",
highlightedItem = bestMatch as? SavableSearchable
)
for (conv in unitConverter) {
SingleResult {
UnitConverterItem(unitConverter = conv)
}
)
CalculatorResults(
calculator,
reverse = reverse
)
CalendarResults(
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(
highlight: Boolean = false,
@ -596,31 +344,15 @@ fun LazyListScope.SingleResult(
}
}
@Composable
fun LazyItemScope.PartialCardRow(
modifier: Modifier = Modifier,
isFirst: Boolean,
isLast: Boolean,
reverse: Boolean,
content: @Composable () -> Unit
) {
val isTop = isFirst && !reverse || isLast && reverse
val isBottom = isLast && !reverse || isFirst && reverse
Box(
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()
}
}
enum class SearchCategory {
Apps,
Calculator,
Calendar,
Contacts,
Files,
UnitConverter,
Wikipedia,
Website,
Location,
Shortcut,
}

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,
item: SavableSearchable,
showLabels: Boolean = true,
labelMaxLines: Int = 1,
highlight: Boolean = false
) {
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}")
@ -181,8 +182,9 @@ fun GridItem(
text = item.labelOverride ?: item.label,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
maxLines = labelMaxLines,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onBackground,
)
}
@ -223,7 +225,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
.fillMaxSize()
.systemBarsPadding()
.imePadding()
.padding(horizontal = 16.dp)
.padding(horizontal = 8.dp)
.then(
if (show.targetState) {
Modifier.pointerInput(Unit) {
@ -242,7 +244,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
modifier = Modifier
.placeOverlay(
origin.translate(
-16.dp.toPixels(),
-8.dp.toPixels(),
-WindowInsets.systemBars.union(WindowInsets.ime)
.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
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
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.geometry.Rect
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.unit.dp
import de.mm20.launcher2.search.AppShortcut
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.ui.component.InnerCard
import de.mm20.launcher2.search.Website
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
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.location.LocationItem
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
@Composable
fun ListItem(
modifier: Modifier = Modifier,
item: SavableSearchable,
highlight: Boolean = false
highlight: Boolean = false,
showDetails: Boolean,
onShowDetails: (Boolean) -> Unit
) {
var showDetails by remember { mutableStateOf(false) }
val context = LocalContext.current
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${item.key}")
@ -41,104 +58,147 @@ fun ListItem(
LaunchedEffect(item, iconSize) {
viewModel.init(item, iconSize.toInt())
}
LaunchedEffect(showDetails) {
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
var bounds by remember { mutableStateOf(Rect.Zero) }
InnerCard(
Box(
modifier = modifier
.background(background)
.onGloballyPositioned {
bounds = it.boundsInWindow()
},
highlight = highlight,
raised = showDetails
) {
when (item) {
is Contact -> {
ContactItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = { showDetails = true },
onLongClick = { showDetails = true }
),
contact = item,
showDetails = showDetails,
onBack = { showDetails = false }
)
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onSurface
) {
when (item) {
is Contact -> {
ContactItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
enabled = !showDetails,
onClick = { onShowDetails(true) },
onLongClick = { onShowDetails(true) }
),
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
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
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.getValue
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.draw.clip
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.layout.BottomReversed
@ -18,18 +27,25 @@ fun SearchResultList(
reverse: Boolean = false,
highlightedItem: SavableSearchable? = null
) {
var selectedIndex by remember { mutableIntStateOf(-1) }
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
) {
for (item in items) {
for ((i, item) in items.withIndex()) {
key(item.key) {
if (i != 0) {
HorizontalDivider()
}
ListItem(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
.fillMaxWidth(),
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
) {
val icon by viewModel.icon.collectAsStateWithLifecycle()
val padding by transition.animateDp(label = "iconPadding") {
if (it) 16.dp else 8.dp
}
val padding = 16.dp
ShapedLauncherIcon(
size = 48.dp,
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(),
exit = slideOut { IntOffset(-it.width, 0) } + fadeOut(),
)
.padding(8.dp),
.padding(12.dp),
size = 48.dp,
icon = { icon },
badge = { badge },
@ -201,7 +201,7 @@ fun LocationItem(
}
Compass(
targetHeading = targetHeading,
modifier = Modifier.padding(end = 8.dp) then
modifier = Modifier.padding(end = 12.dp) then
if (!showMap) {
Modifier.sharedBounds(
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(
modifier = modifier.clickable {
viewModel.launch(context)
}
modifier = modifier
) {
if (!article.imageUrl.isNullOrEmpty()) {
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,
)
}