diff --git a/.idea/emulatorDisplays.xml b/.idea/emulatorDisplays.xml
new file mode 100644
index 00000000..1bb36d9e
--- /dev/null
+++ b/.idea/emulatorDisplays.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt
index 5874ba4e..47f33b95 100644
--- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt
+++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt
@@ -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 }
- }
- }
-
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt
index 3d1d5eeb..e5527d59 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt
@@ -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
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt
index b740aa98..c575d799 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/FavoritesVM.kt
@@ -24,7 +24,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
val selectedTag = MutableStateFlow(null)
- val showEditButton = settings.showEditButton
+ val showEditButton = settings.showEditButton.stateIn(viewModelScope, SharingStarted.Lazily, false)
abstract val tagsExpanded: Flow
val pinnedTags = favoritesService.getFavorites(
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt
index 0e34ade4..8056da3f 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/SearchBar.kt
@@ -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
}
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/ButtonDefaults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/ButtonDefaults.kt
new file mode 100644
index 00000000..d756f8cf
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/ButtonDefaults.kt
@@ -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()
+ )
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/CornerBasedShape.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/CornerBasedShape.kt
new file mode 100644
index 00000000..57a6315d
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/CornerBasedShape.kt
@@ -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 = remember { spring() },
+ visibilityThreshold: CornerBasedShape? = null,
+ label: String = "ValueAnimation",
+ finishedListener: ((CornerBasedShape) -> Unit)? = null
+): State {
+ 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 {
+ 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 {
+ 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)
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt
index e50a1d91..374d777b 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PagerScaffold.kt
@@ -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(
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt
index 74cdf35f..d1843f77 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/PullDownScaffold.kt
@@ -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()
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt
index 4f25ee4a..e7ed2fd7 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt
@@ -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,
- 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,
- 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,
- 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,
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt
new file mode 100644
index 00000000..2e0cc818
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppResults.kt
@@ -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,
+ 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,
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calculator/CalculatorResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calculator/CalculatorResults.kt
new file mode 100644
index 00000000..6285d3f4
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calculator/CalculatorResults.kt
@@ -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,
+ reverse: Boolean,
+) {
+ if (calculator.isNotEmpty()) {
+ item(key = "calculator") {
+ ListItemSurface(
+ isFirst = true,
+ isLast = true,
+ reverse = reverse,
+ ) {
+ CalculatorItem(calculator = calculator.first())
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt
new file mode 100644
index 00000000..851b6abd
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/calendar/CalendarResults.kt
@@ -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,
+ 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
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt
index e01185dc..52e8e5a2 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridItem.kt
@@ -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()
),
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridResults.kt
new file mode 100644
index 00000000..e0d09170
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/grid/GridResults.kt
@@ -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 LazyListScope.GridResults(
+ key: String,
+ items: List,
+ 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()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt
index dd7e3b43..a21f870a 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListItem.kt
@@ -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 }
- )
- }
}
}
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListResults.kt
new file mode 100644
index 00000000..0cedbdea
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/ListResults.kt
@@ -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 LazyListScope.ListResults(
+ key: String,
+ items: List,
+ 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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt
index 4ee9f742..91310adb 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/list/SearchResultList.kt
@@ -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}
)
}
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt
index 29e99d10..158cb60d 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactItem.kt
@@ -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
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt
new file mode 100644
index 00000000..3775d3b8
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/contacts/ContactResults.kt
@@ -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,
+ 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
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/favorites/SearchFavorites.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/favorites/SearchFavorites.kt
new file mode 100644
index 00000000..5262c3a4
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/favorites/SearchFavorites.kt
@@ -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,
+ pinnedTags: List,
+ 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,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt
new file mode 100644
index 00000000..7e787a4d
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/files/FileResults.kt
@@ -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,
+ 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
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt
index 76f22496..7b9d3c14 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationItem.kt
@@ -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"),
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationResults.kt
new file mode 100644
index 00000000..3f2cc678
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/location/LocationResults.kt
@@ -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,
+ 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
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutResults.kt
new file mode 100644
index 00000000..cd11be9b
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/shortcut/ShortcutResults.kt
@@ -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,
+ 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
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterItem.kt
index 068e2d0b..3adfd950 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterItem.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterItem.kt
@@ -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
- }
-}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterResults.kt
new file mode 100644
index 00000000..ac9fa072
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/unitconverter/UnitConverterResults.kt
@@ -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,
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteResults.kt
new file mode 100644
index 00000000..082566c8
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/website/WebsiteResults.kt
@@ -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,
+ 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,
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt
index 4e7b1cca..6c589233 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleItem.kt
@@ -61,9 +61,7 @@ fun ArticleItem(
}
Column(
- modifier = modifier.clickable {
- viewModel.launch(context)
- }
+ modifier = modifier
) {
if (!article.imageUrl.isNullOrEmpty()) {
AsyncImage(
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleResults.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleResults.kt
new file mode 100644
index 00000000..6bd6f7d7
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/wikipedia/ArticleResults.kt
@@ -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,
+ 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,
+ )
+}
\ No newline at end of file