Search result design adjustments
This commit is contained in:
parent
e41d20331f
commit
8875f6922b
47
.idea/emulatorDisplays.xml
generated
Normal file
47
.idea/emulatorDisplays.xml
generated
Normal 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>
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
)
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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()
|
||||
),
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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"),
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
@ -61,9 +61,7 @@ fun ArticleItem(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.clickable {
|
||||
viewModel.launch(context)
|
||||
}
|
||||
modifier = modifier
|
||||
) {
|
||||
if (!article.imageUrl.isNullOrEmpty()) {
|
||||
AsyncImage(
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user