Standardize bottom sheets (#1231)

* move to prebuilt bottom sheet

* improve searchable editing

* fix too much space above widget picker sheet search bar

* improve restore/backup sheets

* import theme and location picker scaffolds

* improve clock widget settings icons

* continue migration

* remove search action delete button

* force maximise tag edit sheet

* add background to icon pick button

* accomodate long button name
This commit is contained in:
leekleak 2025-01-22 18:57:32 +02:00 committed by GitHub
parent 912fddc7c4
commit 024a646b1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 591 additions and 1109 deletions

View File

@ -4,6 +4,7 @@ import android.content.pm.PackageManager
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -22,7 +23,6 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
@ -79,7 +79,7 @@ fun IconPicker(
var showIconPackFilter by remember { mutableStateOf(false) }
val installedIconPacks by viewModel.installedIconPacks.collectAsState(null)
val noPacksInstalled = installedIconPacks?.isEmpty() == true
val packsInstalled = installedIconPacks?.isEmpty() == false
val columns = LocalGridSettings.current.columnCount
@ -88,42 +88,38 @@ fun IconPicker(
columns = GridCells.Fixed(columns),
contentPadding = contentPadding,
) {
item(span = { GridItemSpan(columns) }) {
SearchBar(
modifier = Modifier.padding(bottom = 16.dp),
expanded = false,
onExpandedChange = {},
inputField = {
SearchBarDefaults.InputField(
enabled = !noPacksInstalled,
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
},
onSearch = {},
expanded = false,
onExpandedChange = {},
placeholder = {
Text(
stringResource(
if (noPacksInstalled) R.string.icon_picker_no_packs_installed else R.string.icon_picker_search_icon
if (packsInstalled) {
item(span = { GridItemSpan(columns) }) {
SearchBar(
windowInsets = WindowInsets(0.dp),
expanded = false,
onExpandedChange = {},
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
)
},
query = query,
onQueryChange = {
query = it
scope.launch {
viewModel.searchIcon(query, filterIconPack)
}
},
)
}
) {
},
onSearch = {},
expanded = false,
onExpandedChange = {},
placeholder = {
Text(stringResource(R.string.icon_picker_search_icon))
},
query = query,
onQueryChange = {
query = it
scope.launch {
viewModel.searchIcon(query, filterIconPack)
}
},
)
}
) {
}
}
}
@ -138,11 +134,11 @@ fun IconPicker(
})
}
}
item(span = { GridItemSpan(columns) }) {
Separator(stringResource(R.string.icon_picker_suggestions))
}
if (suggestions.isNotEmpty()) {
item(span = { GridItemSpan(columns) }) {
Separator(stringResource(R.string.icon_picker_suggestions))
}
items(suggestions) {
IconPreview(
it,
@ -152,87 +148,82 @@ fun IconPicker(
}
}
} else {
if (!installedIconPacks.isNullOrEmpty()) {
item(
span = { GridItemSpan(columns) },
item(span = { GridItemSpan(columns) }) {
Button(
onClick = { showIconPackFilter = !showIconPackFilter },
modifier = Modifier
.wrapContentWidth(align = Alignment.CenterHorizontally)
.padding(16.dp),
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 8.dp
)
) {
Button(
onClick = { showIconPackFilter = !showIconPackFilter },
modifier = Modifier
.wrapContentWidth(align = Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 8.dp
)
) {
if (filterIconPack == null) {
Icon(
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
imageVector = Icons.Rounded.FilterAlt,
contentDescription = null
)
} else {
val icon = remember(filterIconPack?.packageName) {
try {
filterIconPack?.packageName?.let { pkg ->
context.packageManager.getApplicationIcon(pkg)
}
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
AsyncImage(
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
model = icon,
contentDescription = null
)
}
DropdownMenu(
expanded = showIconPackFilter,
onDismissRequest = { showIconPackFilter = false }) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.icon_picker_filter_all_packs)) },
onClick = {
showIconPackFilter = false
filterIconPack = null
scope.launch {
viewModel.searchIcon(query, filterIconPack)
}
}
)
installedIconPacks?.forEach { iconPack ->
DropdownMenuItem(
onClick = {
showIconPackFilter = false
filterIconPack = iconPack
scope.launch {
viewModel.searchIcon(query, filterIconPack)
}
},
text = {
Text(iconPack.name)
})
}
}
Text(
text = filterIconPack?.name
?: stringResource(id = R.string.icon_picker_filter_all_packs),
modifier = Modifier.animateContentSize()
)
if (filterIconPack == null) {
Icon(
Icons.Rounded.ArrowDropDown,
modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing)
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
imageVector = Icons.Rounded.FilterAlt,
contentDescription = null
)
} else {
val icon = remember(filterIconPack?.packageName) {
try {
filterIconPack?.packageName?.let { pkg ->
context.packageManager.getApplicationIcon(pkg)
}
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
AsyncImage(
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
model = icon,
contentDescription = null
)
}
DropdownMenu(
expanded = showIconPackFilter,
onDismissRequest = { showIconPackFilter = false }) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.icon_picker_filter_all_packs)) },
onClick = {
showIconPackFilter = false
filterIconPack = null
scope.launch {
viewModel.searchIcon(query, filterIconPack)
}
}
)
installedIconPacks?.forEach { iconPack ->
DropdownMenuItem(
onClick = {
showIconPackFilter = false
filterIconPack = iconPack
scope.launch {
viewModel.searchIcon(query, filterIconPack)
}
},
text = {
Text(iconPack.name)
})
}
}
Text(
text = filterIconPack?.name
?: stringResource(id = R.string.icon_picker_filter_all_packs),
modifier = Modifier.animateContentSize()
)
Icon(
Icons.Rounded.ArrowDropDown,
modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
contentDescription = null
)
}
}

View File

@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DarkMode
import androidx.compose.material.icons.rounded.ErrorOutline
@ -69,21 +67,7 @@ fun ImportThemeSheet(
val error by viewModel.error
var apply by viewModel.apply
BottomSheetDialog(
onDismissRequest = onDismiss,
confirmButton = if (theme != null && !error) {
{
Button(
onClick = {
viewModel.import()
onDismiss()
}
) {
Text(stringResource(R.string.action_import))
}
}
} else null,
) {
BottomSheetDialog(onDismiss) {
if (theme == null && !error) {
Box(
modifier = Modifier
@ -111,8 +95,8 @@ fun ImportThemeSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(it)
.padding(it),
horizontalAlignment = Alignment.End
) {
ThemePreview(
theme!!,
@ -132,6 +116,15 @@ fun ImportThemeSheet(
apply = it
})
}
Button(
modifier = Modifier.padding(top = 8.dp),
onClick = {
viewModel.import()
onDismiss()
},
) {
Text(stringResource(R.string.action_import))
}
}
}
}

View File

@ -2,12 +2,22 @@ package de.mm20.launcher2.ui.common
import android.net.Uri
import android.text.format.DateUtils
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -37,35 +47,12 @@ fun RestoreBackupSheet(
val state by viewModel.state
val compatibility by viewModel.compatibility
BottomSheetDialog(
onDismissRequest = onDismissRequest,
title = {
Text(
stringResource(id = R.string.preference_restore),
)
},
confirmButton = {
if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
Button(
onClick = { viewModel.restore() }) {
Text(stringResource(R.string.preference_restore))
}
} else if (state == RestoreBackupState.InvalidFile || state == RestoreBackupState.Restored || state == RestoreBackupState.Ready) {
OutlinedButton(
onClick = onDismissRequest
) {
Text(stringResource(R.string.close))
}
}
},
) {
Box(
BottomSheetDialog(onDismissRequest) {
Column (
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.verticalScroll(rememberScrollState())
.padding(it)
.padding(it),
horizontalAlignment = Alignment.End,
) {
when (state) {
RestoreBackupState.Parsing -> {
@ -159,6 +146,14 @@ fun RestoreBackupSheet(
)
}
}
if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
Button(
onClick = { viewModel.restore() }
) {
Text(stringResource(R.string.preference_restore))
}
}
}
}
}

View File

@ -36,12 +36,11 @@ import de.mm20.launcher2.ui.ktx.toPixels
fun SearchablePicker(
value: SavableSearchable?,
onValueChanged: (SavableSearchable?) -> Unit,
title: @Composable () -> Unit,
onDismissRequest: () -> Unit,
) {
val viewModel: SearchablePickerVM = viewModel()
BottomSheetDialog(onDismissRequest = onDismissRequest, title = title) {
BottomSheetDialog(onDismissRequest = onDismissRequest) {
Column(
modifier = Modifier
.fillMaxSize()

View File

@ -2,7 +2,7 @@ package de.mm20.launcher2.ui.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -11,8 +11,17 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -32,38 +41,39 @@ fun WeatherLocationSearchDialog(
val isSearching by viewModel.isSearchingLocation
val locations by viewModel.locationResults
BottomSheetDialog(
onDismissRequest = onDismissRequest,
title = {
Text(
text = stringResource(R.string.preference_location),
)
},
) {
BottomSheetDialog(onDismissRequest) {
var query by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize().padding(it)
modifier = Modifier
.fillMaxSize()
.padding(it)
) {
Row(
Modifier.padding(bottom = 16.dp)
) {
OutlinedTextField(
singleLine = true,
value = query,
textStyle = MaterialTheme.typography.bodyLarge,
onValueChange = {
query = it
scope.launch {
viewModel.searchLocation(it)
}
},
leadingIcon = {
Icon(imageVector = Icons.Rounded.Search, contentDescription = null)
},
modifier = Modifier
.weight(1f)
)
}
SearchBar(
modifier = Modifier.padding(bottom = 8.dp),
windowInsets = WindowInsets(0.dp),
expanded = false,
onExpandedChange = {},
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
},
onSearch = {},
expanded = false,
onExpandedChange = {},
query = query,
onQueryChange = {
query = it
scope.launch {
viewModel.searchLocation(it)
}
},
)
}
) {}
if (isSearching) {
CircularProgressIndicator(
modifier = Modifier

View File

@ -1,374 +1,42 @@
package de.mm20.launcher2.ui.component
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupPositionProvider
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.overlays.LocalZIndex
import de.mm20.launcher2.ui.overlays.Overlay
import kotlinx.coroutines.launch
import kotlin.math.min
import kotlin.math.roundToInt
@Composable
fun BottomSheetDialog(
onDismissRequest: () -> Unit,
title: (@Composable () -> Unit)? = null,
actions: (@Composable RowScope.() -> Unit)? = null,
confirmButton: @Composable (() -> Unit)? = null,
dismissButton: @Composable (() -> Unit)? = null,
dismissible: () -> Boolean = { true },
zIndex: Float = LocalZIndex.current + 1f,
footerItems: @Composable (() -> Unit)? = null,
bottomSheetState: SheetState = rememberModalBottomSheetState(),
content: @Composable (paddingValues: PaddingValues) -> Unit,
) {
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
focusManager.clearFocus(true)
}
var isOpenAnimationFinished by remember { mutableStateOf(false) }
val draggableState = remember {
AnchoredDraggableState(
initialValue = SwipeState.Dismiss,
positionalThreshold = { it * 0.5f },
velocityThreshold = { 200f },
snapAnimationSpec = spring(),
decayAnimationSpec = exponentialDecay(),
confirmValueChange = {
it != SwipeState.Dismiss || dismissible()
}
)
}
BackHandler {
if (dismissible()) {
scope.launch {
draggableState.animateTo(SwipeState.Dismiss)
onDismissRequest()
}
}
}
LaunchedEffect(draggableState.settledValue) {
if (isOpenAnimationFinished && draggableState.settledValue == SwipeState.Dismiss) {
onDismissRequest()
} else {
isOpenAnimationFinished = true
}
}
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.toFloat()
return if (delta < 0 && source == NestedScrollSource.Drag) {
draggableState.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return if (source == NestedScrollSource.Drag) {
draggableState.dispatchRawDelta(available.toFloat()).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = Offset(available.x, available.y).toFloat()
return if (toFling < 0 && draggableState.offset > draggableState.anchors.minPosition()) {
draggableState.settle(velocity = toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available
} else {
Velocity.Zero
}
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
draggableState.settle(velocity = Offset(available.x, available.y).toFloat())
return available
}
private fun Float.toOffset(): Offset = Offset(0f, this)
private fun Offset.toFloat(): Float = this.y
}
}
CompositionLocalProvider(
LocalAbsoluteTonalElevation provides 0.dp,
ModalBottomSheet(
sheetState = bottomSheetState,
onDismissRequest = onDismissRequest,
) {
Overlay(zIndex = zIndex) {
BoxWithConstraints(
content(PaddingValues(horizontal = 24.dp, vertical = 8.dp))
if (footerItems != null) {
Row(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding(),
propagateMinConstraints = true,
contentAlignment = Alignment.BottomCenter
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.End,
)
) {
val maxHeight = maxHeight
val scrimAlpha by animateFloatAsState(
if (draggableState.targetValue == SwipeState.Dismiss) 0f else 0.32f,
label = "Scrim alpha"
)
Box(modifier = Modifier
.background(MaterialTheme.colorScheme.scrim.copy(alpha = scrimAlpha))
.fillMaxSize()
.pointerInput(onDismissRequest, dismissible) {
detectTapGestures {
if (dismissible()) {
scope.launch {
draggableState.animateTo(SwipeState.Dismiss)
onDismissRequest()
}
}
}
}
)
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.clipToBounds(),
contentAlignment = Alignment.TopCenter,
) {
var sheetHeight by remember {
mutableStateOf(0f)
}
val maxHeightPx = maxHeight.toPixels()
LaunchedEffect(maxHeightPx, sheetHeight) {
val oldValue = draggableState.currentValue
val hasPeekAnchor = sheetHeight > 0f
val hasFullAnchor = sheetHeight > maxHeightPx * 0.5f
// If the sheet was hidden, move it to peek. Otherwise, try to keep the previous state, if possible.
val newValue = when {
oldValue == SwipeState.Dismiss && hasPeekAnchor -> SwipeState.Peek
oldValue == SwipeState.Peek && hasPeekAnchor -> SwipeState.Peek
oldValue == SwipeState.Full && hasFullAnchor -> SwipeState.Full
oldValue == SwipeState.Full && hasPeekAnchor -> SwipeState.Peek
else -> SwipeState.Dismiss
}
val newAnchors = DraggableAnchors {
SwipeState.Dismiss at 0f
if (hasPeekAnchor) SwipeState.Peek at -min(
maxHeightPx * 0.5f,
sheetHeight
)
if (hasFullAnchor) SwipeState.Full at -min(maxHeightPx, sheetHeight)
}
draggableState.updateAnchors(
newAnchors,
with(draggableState) {
(if (!offset.isNaN()) {
newAnchors.closestAnchor(offset) ?: targetValue
} else targetValue).let {
if (it == SwipeState.Dismiss && targetValue != SwipeState.Dismiss && hasPeekAnchor) SwipeState.Peek else it
}
},
)
if (newValue != oldValue) {
draggableState.animateTo(newValue)
}
}
Surface(
modifier = Modifier
.nestedScroll(nestedScrollConnection)
.onSizeChanged {
sheetHeight = it.height.toFloat()
}
.offset {
IntOffset(0,
maxHeightPx.toInt() +
(draggableState.offset
.takeIf { !it.isNaN() }
?.roundToInt() ?: 0)
)
}
.anchoredDraggable(
state = draggableState,
orientation = Orientation.Vertical,
)
.fillMaxWidth(),
shape = MaterialTheme.shapes.extraLarge.copy(
bottomStart = CornerSize(0),
bottomEnd = CornerSize(0),
),
shadowElevation = 16.dp,
tonalElevation = 1.dp,
color = MaterialTheme.colorScheme.surfaceContainerLow,
) {
Column {
if (title != null || actions != null) {
CenterAlignedTopAppBar(
title = title ?: { BottomSheetDefaults.DragHandle() },
actions = actions ?: {},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent,
),
)
} else {
Box(
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
BottomSheetDefaults.DragHandle()
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.then(
if (confirmButton != null || dismissButton != null) Modifier.padding(
bottom = 64.dp
) else Modifier
)
.wrapContentHeight(),
propagateMinConstraints = true,
contentAlignment = Alignment.Center
) {
content(PaddingValues(horizontal = 24.dp, vertical = 8.dp))
}
}
}
if (confirmButton != null || dismissButton != null) {
val elevation = 1.dp
val heightPx = -64.dp.toPixels()
Surface(
modifier = Modifier
.height(64.dp)
.offset {
IntOffset(
0,
maxHeightPx.toInt() +
(draggableState.offset
.takeIf { !it.isNaN() }
?.roundToInt()
?: 0).coerceAtLeast(heightPx.toInt())
)
}
.fillMaxWidth(),
tonalElevation = elevation,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.End
) {
if (dismissButton != null) {
dismissButton()
}
if (confirmButton != null && dismissButton != null) {
Spacer(modifier = Modifier.width(16.dp))
}
if (confirmButton != null) {
confirmButton()
}
}
}
}
}
footerItems()
}
}
}
}
private enum class SwipeState {
Full, Peek, Dismiss
}
private class BottomSheetPositionProvider(val insets: WindowInsets, val density: Density) :
PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
return IntOffset.Zero + IntOffset(
insets.getLeft(density, layoutDirection),
insets.getTop(density)
)
}
}

View File

@ -106,17 +106,7 @@ fun ConfigureWidgetSheet(
onWidgetUpdated: (Widget) -> Unit,
onDismiss: () -> Unit,
) {
BottomSheetDialog(onDismissRequest = onDismiss,
title = {
Box(
modifier = Modifier
.width(32.dp)
.height(4.dp)
.clip(MaterialTheme.shapes.small)
.background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f))
)
}
) {
BottomSheetDialog(onDismissRequest = onDismiss) {
Column(
modifier = Modifier
.fillMaxWidth()

View File

@ -1,44 +1,27 @@
package de.mm20.launcher2.ui.launcher.sheets
import android.content.pm.PackageManager
import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Label
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -50,16 +33,13 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeIcon
import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.SavableSearchable
@ -70,7 +50,6 @@ import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalGridSettings
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@ -79,246 +58,231 @@ fun CustomizeSearchableSheet(
searchable: SavableSearchable,
onDismiss: () -> Unit,
) {
val scope = rememberCoroutineScope()
val viewModel: CustomizeSearchableSheetVM =
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
val pickIcon by viewModel.isIconPickerOpen
if (pickIcon) {
BackHandler {
viewModel.closeIconPicker()
}
}
BottomSheetDialog(onDismissRequest = { if (!pickIcon) onDismiss() }) {
Column(
modifier = Modifier.padding(it),
horizontalAlignment = Alignment.CenterHorizontally
) {
val iconSize = 64.dp
val iconSizePx = iconSize.toPixels()
val icon by remember { viewModel.getIcon(iconSizePx.toInt()) }.collectAsState(null)
BottomSheetDialog(
onDismissRequest = onDismiss,
title = if (pickIcon) {
{
Text(stringResource(R.string.icon_picker_title))
}
} else null,
dismissible = { !pickIcon },
confirmButton = if (pickIcon) {
{
OutlinedButton(onClick = { viewModel.closeIconPicker() }) {
Text(stringResource(id = android.R.string.cancel))
ShapedLauncherIcon(
size = iconSize,
icon = { icon },
badge = {
Badge(
icon = BadgeIcon(Icons.Rounded.Edit)
)
},
modifier = Modifier.clickable {
viewModel.openIconPicker()
}
)
var customLabelValue by remember {
mutableStateOf(searchable.labelOverride ?: "")
}
} else null,
zIndex = 100f,
) {
if (!pickIcon) {
Column(
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp, bottom = 16.dp),
value = customLabelValue,
onValueChange = {
customLabelValue = it
},
singleLine = true,
label = {
Text(stringResource(R.string.customize_item_label))
},
placeholder = {
Text(searchable.label)
},
leadingIcon = {
Icon(Icons.AutoMirrored.Rounded.Label, null)
}
)
var tags by remember { mutableStateOf(emptyList<String>()) }
var visibility by remember { mutableStateOf(VisibilityLevel.Default) }
LaunchedEffect(searchable.key) {
visibility = viewModel.getVisibility().first()
tags = viewModel.getTags().first()
}
OutlinedTagsInputField(
modifier = Modifier
.padding(top = 8.dp)
.padding(it),
horizontalAlignment = Alignment.CenterHorizontally
) {
val iconSize = 64.dp
val iconSizePx = iconSize.toPixels()
val icon by remember { viewModel.getIcon(iconSizePx.toInt()) }.collectAsState(null)
ShapedLauncherIcon(
size = iconSize,
icon = { icon },
badge = {
Badge(
icon = BadgeIcon(Icons.Rounded.Edit)
)
},
modifier = Modifier.clickable {
viewModel.openIconPicker()
}
)
var customLabelValue by remember {
mutableStateOf(searchable.labelOverride ?: "")
.fillMaxWidth(),
tags = tags, onTagsChange = { tags = it.distinct() },
label = {
Text(stringResource(R.string.customize_item_tags))
},
onAutocomplete = {
viewModel.autocompleteTags(it).minus(tags.toSet())
},
leadingIcon = {
Icon(Icons.Rounded.Tag, null)
}
)
var showDropdown by remember {
mutableStateOf(false)
}
ExposedDropdownMenuBox(
expanded = showDropdown,
onExpandedChange = { showDropdown = it },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp, bottom = 16.dp),
value = customLabelValue,
onValueChange = {
customLabelValue = it
},
singleLine = true,
label = {
Text(stringResource(R.string.customize_item_label))
},
placeholder = {
Text(searchable.label)
},
leadingIcon = {
Icon(Icons.AutoMirrored.Rounded.Label, null)
}
)
var tags by remember { mutableStateOf(emptyList<String>()) }
var visibility by remember { mutableStateOf(VisibilityLevel.Default) }
LaunchedEffect(searchable.key) {
visibility = viewModel.getVisibility().first()
tags = viewModel.getTags().first()
}
OutlinedTagsInputField(
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
tags = tags, onTagsChange = { tags = it.distinct() },
label = {
Text(stringResource(R.string.customize_item_tags))
},
onAutocomplete = {
viewModel.autocompleteTags(it).minus(tags.toSet())
},
leadingIcon = {
Icon(Icons.Rounded.Tag, null)
}
)
var showDropdown by remember {
mutableStateOf(false)
}
ExposedDropdownMenuBox(
expanded = showDropdown,
onExpandedChange = { showDropdown = it },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
value = when (visibility) {
VisibilityLevel.Default -> {
when (searchable) {
is Application -> stringResource(R.string.item_visibility_app_default)
is CalendarEvent -> stringResource(R.string.item_visibility_calendar_default)
else -> stringResource(R.string.item_visibility_search_only)
}
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
value = when (visibility) {
VisibilityLevel.Default -> {
when (searchable) {
is Application -> stringResource(R.string.item_visibility_app_default)
is CalendarEvent -> stringResource(R.string.item_visibility_calendar_default)
else -> stringResource(R.string.item_visibility_search_only)
}
}
VisibilityLevel.SearchOnly -> stringResource(R.string.item_visibility_search_only)
VisibilityLevel.Hidden -> stringResource(R.string.item_visibility_hidden)
},
label = {
Text(stringResource(R.string.customize_item_visibility))
},
onValueChange = {},
readOnly = true,
singleLine = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showDropdown) },
leadingIcon = {
Icon(
when (visibility) {
VisibilityLevel.Default -> Icons.Rounded.Visibility
VisibilityLevel.SearchOnly -> Icons.Outlined.Visibility
VisibilityLevel.Hidden -> Icons.Rounded.VisibilityOff
},
null
)
},
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
)
ExposedDropdownMenu(
expanded = showDropdown,
onDismissRequest = {
showDropdown = false
}
) {
if (searchable is Application) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_app_default))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
} else if (searchable is CalendarEvent) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_calendar_default))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
} else {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_search_only))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
}
if (searchable is Application || searchable is CalendarEvent) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.SearchOnly
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_search_only))
},
leadingIcon = {
Icon(
Icons.Outlined.Visibility,
null
)
}
)
}
VisibilityLevel.SearchOnly -> stringResource(R.string.item_visibility_search_only)
VisibilityLevel.Hidden -> stringResource(R.string.item_visibility_hidden)
},
label = {
Text(stringResource(R.string.customize_item_visibility))
},
onValueChange = {},
readOnly = true,
singleLine = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showDropdown) },
leadingIcon = {
Icon(
when (visibility) {
VisibilityLevel.Default -> Icons.Rounded.Visibility
VisibilityLevel.SearchOnly -> Icons.Outlined.Visibility
VisibilityLevel.Hidden -> Icons.Rounded.VisibilityOff
},
null
)
},
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
)
ExposedDropdownMenu(
expanded = showDropdown,
onDismissRequest = {
showDropdown = false
}
) {
if (searchable is Application) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Hidden
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_hidden))
Text(stringResource(R.string.item_visibility_app_default))
},
leadingIcon = {
Icon(Icons.Rounded.VisibilityOff, null)
Icon(Icons.Rounded.Visibility, null)
}
)
} else if (searchable is CalendarEvent) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_calendar_default))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
} else {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_search_only))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
}
}
DisposableEffect(searchable.key) {
onDispose {
viewModel.setCustomLabel(customLabelValue)
viewModel.setTags(tags)
viewModel.setVisibility(visibility)
if (searchable is Application || searchable is CalendarEvent) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.SearchOnly
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_search_only))
},
leadingIcon = {
Icon(
Icons.Outlined.Visibility,
null
)
}
)
}
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Hidden
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_hidden))
},
leadingIcon = {
Icon(Icons.Rounded.VisibilityOff, null)
}
)
}
}
} else {
IconPicker(
searchable = searchable,
onSelect = {
viewModel.pickIcon(it)
},
contentPadding = it,
)
DisposableEffect(searchable.key) {
onDispose {
viewModel.setCustomLabel(customLabelValue)
viewModel.setTags(tags)
viewModel.setVisibility(visibility)
}
}
}
if (pickIcon) {
val bottomSheetState = rememberModalBottomSheetState()
BottomSheetDialog (
onDismissRequest = { viewModel.closeIconPicker() },
bottomSheetState = bottomSheetState
) {
IconPicker(
searchable = searchable,
onSelect = {
scope.launch {
viewModel.pickIcon(it)
bottomSheetState.hide()
viewModel.closeIconPicker()
}
},
contentPadding = it,
)
}
}
}
}

View File

@ -45,7 +45,6 @@ class CustomizeSearchableSheetVM(
fun pickIcon(icon: CustomIcon?) {
iconService.setCustomIcon(searchable, icon)
closeIconPicker()
}
fun setCustomLabel(label: String) {

View File

@ -39,7 +39,6 @@ import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
@ -106,28 +105,7 @@ fun EditFavoritesSheet(
val loading by viewModel.loading
val createShortcutTarget by viewModel.createShortcutTarget
BottomSheetDialog(
onDismissRequest = onDismiss,
title = {
Text(
if (createShortcutTarget == null) {
stringResource(id = R.string.menu_item_edit_favs)
} else {
stringResource(id = R.string.create_app_shortcut)
}
)
},
dismissible = {
createShortcutTarget == null
},
confirmButton = if (createShortcutTarget != null) {
{
OutlinedButton(onClick = { viewModel.cancelPickShortcut() }) {
Text(stringResource(id = android.R.string.cancel))
}
}
} else null
) {
BottomSheetDialog(onDismiss) {
if (loading) {
Box(
modifier = Modifier

View File

@ -86,6 +86,8 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
.map { Tag(it) }
this.pinnedTags.value = pinnedTags
createShortcutTarget.value = null
buildItemList()
loading.value = false
}

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
@ -31,14 +32,15 @@ import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -49,6 +51,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
@ -77,8 +80,6 @@ fun EditTagSheet(
) {
val viewModel: EditTagSheetVM = viewModel()
val isCreatingNewTag = tag == null
val density = LocalDensity.current
LaunchedEffect(tag) {
@ -88,48 +89,14 @@ fun EditTagSheet(
if (viewModel.loading) return
BottomSheetDialog(
title = {
Text(
stringResource(
if (viewModel.page == EditTagSheetPage.CustomizeTag || !isCreatingNewTag) R.string.edit_tag_title
else R.string.create_tag_title
)
)
},
confirmButton = if (viewModel.page == EditTagSheetPage.CustomizeTag) {
null
} else if (isCreatingNewTag) {
{
Button(
enabled = (viewModel.tagName.isNotBlank() && viewModel.page == EditTagSheetPage.CreateTag && !viewModel.tagNameExists)
|| (viewModel.page == EditTagSheetPage.PickItems && viewModel.taggedItems.isNotEmpty()),
onClick = { viewModel.onClickContinue() }) {
Text(stringResource(R.string.action_next))
}
}
} else if (viewModel.page == EditTagSheetPage.PickItems) {
{
OutlinedButton(onClick = { viewModel.closeItemPicker() }) {
Text(stringResource(id = R.string.ok))
}
}
} else {
{
OutlinedButton(onClick = { viewModel.closeIconPicker() }) {
Text(stringResource(id = android.R.string.cancel))
}
}
},
bottomSheetState = rememberModalBottomSheetState(true),
onDismissRequest = {
if (viewModel.page == EditTagSheetPage.CustomizeTag) {
viewModel.save()
onTagSaved(viewModel.tagName)
}
onDismiss()
},
dismissible = {
!(!isCreatingNewTag && viewModel.page == EditTagSheetPage.PickItems)
},
}
) {
when (viewModel.page) {
EditTagSheetPage.CreateTag -> CreateNewTagPage(viewModel, it)
@ -161,55 +128,91 @@ fun CreateNewTagPage(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
value = viewModel.tagName,
onValueChange = { viewModel.tagName = it }
)
Button(
modifier = Modifier.align(Alignment.End),
enabled = (viewModel.tagName.isNotBlank() && viewModel.page == EditTagSheetPage.CreateTag && !viewModel.tagNameExists)
|| (viewModel.page == EditTagSheetPage.PickItems && viewModel.taggedItems.isNotEmpty()),
onClick = { viewModel.onClickContinue() }) {
Text(stringResource(R.string.action_next))
}
}
}
@Composable
fun PickItems(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
val columns = LocalGridSettings.current.columnCount - 1
LazyVerticalGrid(
modifier = Modifier.fillMaxWidth(),
columns = GridCells.Fixed(columns),
contentPadding = paddingValues,
Scaffold (
contentWindowInsets = WindowInsets(0.dp),
modifier = Modifier.padding(paddingValues),
containerColor = Color.Transparent,
bottomBar = {
Surface (
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.95f),
shape = MaterialTheme.shapes.medium
) {
Box {
Button(
onClick = { viewModel.closeItemPicker() },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(all = 8.dp)
.padding(end = 12.dp)
) {
Text(stringResource(R.string.action_next))
}
}
}
}
) {
item(span = { GridItemSpan(columns) }) {
Text(
stringResource(id = R.string.tag_select_items),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(bottom = 8.dp)
)
}
items(viewModel.taggableApps) {
val iconSize = 32.dp.toPixels()
val icon by remember(it.item.key) {
viewModel.getIcon(it.item, iconSize.toInt())
}.collectAsState(null)
ListItem(item = it, icon = icon, onTagChanged = { tagged ->
if (tagged) viewModel.tagItem(it.item)
else viewModel.untagItem(it.item)
})
}
LazyVerticalGrid(
modifier = Modifier.fillMaxWidth(),
columns = GridCells.Fixed(columns),
contentPadding = it
) {
item(span = { GridItemSpan(columns) }) {
Text(
stringResource(id = R.string.tag_select_items),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(bottom = 8.dp)
)
}
items(viewModel.taggableApps) {
val iconSize = 32.dp.toPixels()
val icon by remember(it.item.key) {
viewModel.getIcon(it.item, iconSize.toInt())
}.collectAsState(null)
ListItem(item = it, icon = icon, onTagChanged = { tagged ->
if (tagged) viewModel.tagItem(it.item)
else viewModel.untagItem(it.item)
})
}
item(span = { GridItemSpan(columns) }) {
Box(
modifier = Modifier
.padding(vertical = 8.dp)
.background(MaterialTheme.colorScheme.outlineVariant)
.fillMaxWidth()
.height(1.dp)
)
}
if (viewModel.taggableOther.isNotEmpty()) {
item(span = { GridItemSpan(columns) }) {
Box(
modifier = Modifier
.padding(vertical = 8.dp)
.background(MaterialTheme.colorScheme.outlineVariant)
.fillMaxWidth()
.height(1.dp)
)
}
items(viewModel.taggableOther) {
val iconSize = 32.dp.toPixels()
val icon by remember(it.item.key) {
viewModel.getIcon(it.item, iconSize.toInt())
}.collectAsState(null)
ListItem(item = it, icon = icon, onTagChanged = { tagged ->
if (tagged) viewModel.tagItem(it.item)
else viewModel.untagItem(it.item)
})
items(viewModel.taggableOther) {
val iconSize = 32.dp.toPixels()
val icon by remember(it.item.key) {
viewModel.getIcon(it.item, iconSize.toInt())
}.collectAsState(null)
ListItem(item = it, icon = icon, onTagChanged = { tagged ->
if (tagged) viewModel.tagItem(it.item)
else viewModel.untagItem(it.item)
})
}
}
}
}
}
@ -284,7 +287,7 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
.clickable {
viewModel.openIconPicker()
}
.size(56.dp)
.size(72.dp)
then (
if (tagIcon != null) {
Modifier
@ -327,7 +330,7 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
OutlinedTextField(
modifier = Modifier.weight(1f),
singleLine = true,
placeholder = { Text(stringResource(R.string.tag_name)) },
label = { Text(stringResource(R.string.tag_name)) },
value = viewModel.tagName,
onValueChange = { viewModel.tagName = it },
)
@ -391,12 +394,14 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
}
}
}
AnimatedVisibility(viewModel.tagNameExists || viewModel.taggedItems.isEmpty()) {
AnimatedVisibility(viewModel.tagNameExists || viewModel.taggedItems.isEmpty() || viewModel.tagName.isEmpty()) {
SmallMessage(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Rounded.Warning,
text = stringResource(
if (viewModel.taggedItems.isEmpty()) R.string.tag_no_items_message else R.string.tag_exists_message
if (viewModel.taggedItems.isEmpty()) R.string.tag_no_items_message
else if (viewModel.tagNameExists) R.string.tag_exists_message
else R.string.tag_empty_name
)
)
}

View File

@ -20,9 +20,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -83,7 +81,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
val oldName = oldTagName
val newName = tagName
val tagIcon = tagCustomIcon
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
if ((taggedItems.isEmpty() || tagName.isEmpty()) && oldName != null) tagService.deleteTag(oldName)
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
else tagService.createTag(tagName, taggedItems)

View File

@ -47,7 +47,6 @@ fun FailedGestureSheet(
})
BottomSheetDialog(
title = { Text(actionName) },
onDismissRequest = onDismiss,
) {
Column(

View File

@ -3,21 +3,9 @@ package de.mm20.launcher2.ui.launcher.sheets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.SavableSearchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
@ -26,27 +14,7 @@ fun HiddenItemsSheet(
items: List<SavableSearchable>,
onDismiss: () -> Unit
) {
val viewModel: HiddenItemsSheetVM = viewModel()
val context = LocalContext.current
BottomSheetDialog(
onDismissRequest = onDismiss,
title = {
Text(
stringResource(R.string.preference_hidden_items),
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 16.dp),
maxLines = 1
)
},
actions = {
IconButton(onClick = { viewModel.showHiddenItems(context) }) {
Icon(imageVector = Icons.Rounded.Edit, contentDescription = null)
}
},
) {
BottomSheetDialog(onDismiss) {
SearchResultGrid(
items,
modifier = Modifier

View File

@ -19,6 +19,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -64,9 +65,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.AppWidget
import de.mm20.launcher2.widgets.AppWidgetConfig
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.NotesWidget
@ -277,10 +278,8 @@ fun WidgetPickerSheet(
val query by viewModel.searchQuery.collectAsState("")
BottomSheetDialog(
onDismissRequest = onDismiss,
title = {
Text(title)
}) {
onDismissRequest = onDismiss
) {
val builtIn by viewModel.builtInWidgets.collectAsState(emptyList())
LazyColumn(
modifier = Modifier
@ -299,6 +298,7 @@ fun WidgetPickerSheet(
)
}
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
windowInsets = WindowInsets(0.dp),
query = query,
onQueryChange = { viewModel.search(it) },
onSearch = {},

View File

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.rounded.AccessTime
import androidx.compose.material.icons.rounded.Alarm
import androidx.compose.material.icons.rounded.AlignVerticalBottom
@ -370,11 +371,16 @@ fun ConfigureClockWidgetSheet(
icon = {
SegmentedButtonDefaults.Icon(
active = compact == false,
activeContent = {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
)
}
) {
Icon(
imageVector = Icons.Rounded.HorizontalSplit,
contentDescription = null,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
)
}
}
@ -390,11 +396,16 @@ fun ConfigureClockWidgetSheet(
icon = {
SegmentedButtonDefaults.Icon(
active = compact == true,
activeContent = {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
)
}
) {
Icon(
imageVector = Icons.Rounded.VerticalSplit,
contentDescription = null,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
)
}
}

View File

@ -7,7 +7,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
@ -43,11 +42,13 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -57,7 +58,6 @@ 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.Brush
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
@ -398,39 +398,11 @@ fun NoteWidgetConflictResolveSheet(
onDismissRequest: () -> Unit,
) {
var selectedStrategy by remember { mutableStateOf<LinkedFileConflictStrategy?>(null) }
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
BottomSheetDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
Button(
onClick = { onResolve(selectedStrategy ?: return@Button) },
enabled = selectedStrategy != null,
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
Icons.Rounded.CheckCircleOutline,
null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.note_widget_conflict_keep_selected))
}
},
dismissButton = {
TextButton(
onClick = { onResolve(LinkedFileConflictStrategy.Unlink) },
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
) {
Icon(
Icons.Rounded.LinkOff,
null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.note_widget_action_unlink_file))
}
}
bottomSheetState = bottomSheetState
) {
Column(
modifier = Modifier
@ -463,6 +435,40 @@ fun NoteWidgetConflictResolveSheet(
selected = selectedStrategy == LinkedFileConflictStrategy.KeepFile,
onSelect = { selectedStrategy = LinkedFileConflictStrategy.KeepFile },
)
Column (
modifier = Modifier.padding(top = 8.dp),
){
OutlinedButton (
onClick = { onResolve(LinkedFileConflictStrategy.Unlink) },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
),
) {
Icon(
Icons.Rounded.LinkOff,
null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.note_widget_action_unlink_file))
}
Button(
onClick = { onResolve(selectedStrategy ?: return@Button) },
enabled = selectedStrategy != null,
modifier = Modifier.fillMaxWidth()
) {
Icon(
Icons.Rounded.CheckCircleOutline,
null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.note_widget_conflict_keep_selected))
}
}
}
}
}
@ -504,21 +510,6 @@ fun SelectableNoteContent(
.padding(16.dp),
text = content, onTextChange = {}
)
if (!expanded) {
Canvas(
modifier = Modifier
.height(32.dp)
.fillMaxWidth()
.align(Alignment.BottomCenter)
) {
drawRect(
brush = Brush.verticalGradient(
0f to color.copy(alpha = 0f),
1f to color.copy(alpha = 0.8f),
),
)
}
}
IconButton(
modifier = Modifier.align(Alignment.TopEnd),
onClick = onSelect

View File

@ -2,26 +2,24 @@ package de.mm20.launcher2.ui.settings.backup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.SmallMessage
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@ -50,68 +48,29 @@ fun CreateBackupSheet(
val state by viewModel.state
BottomSheetDialog(
onDismissRequest = onDismissRequest,
title = {
Text(
stringResource(id = R.string.preference_backup),
)
},
confirmButton = {
if (state == CreateBackupState.Ready) {
Button(
onClick = {
val fileName = "${
ZonedDateTime.now().format(
DateTimeFormatter.ISO_INSTANT
)
}.kvaesitso"
backupLauncher.launch(fileName)
}) {
Text(stringResource(R.string.preference_backup))
}
} else if (state == CreateBackupState.BackedUp) {
OutlinedButton(
onClick = onDismissRequest
if (state == CreateBackupState.BackingUp || state == CreateBackupState.BackedUp) {
BottomSheetDialog(onDismissRequest) {
if (state == CreateBackupState.BackingUp) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentAlignment = Alignment.Center
) {
Text(stringResource(R.string.close))
}
}
},
) {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.verticalScroll(rememberScrollState())
.padding(it)
) {
when (state) {
CreateBackupState.Ready -> {
}
CreateBackupState.BackingUp -> {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
)
}
}
CreateBackupState.BackedUp -> {
LargeMessage(
modifier = Modifier.aspectRatio(1f),
icon = Icons.Rounded.CheckCircleOutline,
text = stringResource(
id = R.string.backup_complete
)
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
)
}
}
else {
LargeMessage(
modifier = Modifier.aspectRatio(1f),
icon = Icons.Rounded.CheckCircleOutline,
text = stringResource(
id = R.string.backup_complete
)
)
}
}
}
}

View File

@ -279,7 +279,6 @@ fun GesturePreference(
if (!isOpenSearch && value is GestureAction.Launch && (showAppPicker || app == null)) {
SearchablePicker(
title = { Text(title) },
onDismissRequest = {
showAppPicker = false
if (app == null) onValueChanged(GestureAction.NoAction)

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -31,9 +32,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.ManageSearch
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material.icons.rounded.ToggleOn
import androidx.compose.material.icons.rounded.TravelExplore
@ -56,7 +55,6 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -85,8 +83,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import de.mm20.launcher2.searchactions.builders.AppSearchActionBuilder
import de.mm20.launcher2.searchactions.builders.CustomIntentActionBuilder
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
import de.mm20.launcher2.searchactions.builders.CustomWebsearchActionBuilder
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ExperimentalBadge
@ -99,13 +97,11 @@ fun EditSearchActionSheet(
initialSearchAction: CustomizableSearchActionBuilder?,
onSave: (CustomizableSearchActionBuilder) -> Unit,
onDismiss: () -> Unit,
onDelete: () -> Unit = {},
) {
val viewModel: EditSearchActionSheetVM = viewModel()
LaunchedEffect(initialSearchAction) {
viewModel.init(initialSearchAction)
}
val createNew by viewModel.createNew
val page by viewModel.currentPage
val searchAction by viewModel.searchAction
@ -114,10 +110,7 @@ fun EditSearchActionSheet(
viewModel.onDismiss()
onDismiss()
},
dismissible = {
page != EditSearchActionPage.PickIcon
},
confirmButton = when (page) {
footerItems = when (page) {
EditSearchActionPage.CustomizeAppSearch,
EditSearchActionPage.CustomizeWebSearch,
EditSearchActionPage.CustomizeCustomIntent -> {
@ -170,37 +163,6 @@ fun EditSearchActionSheet(
}
else -> null
},
actions = {
var showMenu by remember { mutableStateOf(false) }
if (!createNew) {
IconButton(onClick = { showMenu = !showMenu }) {
Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = null)
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.menu_delete)) },
leadingIcon = {
Icon(imageVector = Icons.Rounded.Delete, contentDescription = null)
},
onClick = {
onDelete()
showMenu = false
}
)
}
}
}
},
title = {
Text(
stringResource(
if (createNew) {
R.string.create_search_action_title
} else {
R.string.edit_search_action_title
}
)
)
}) {
when (page) {
EditSearchActionPage.SelectType -> SelectTypePage(viewModel, it)
@ -741,7 +703,7 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
if (action?.customIcon == null) {
val availableIcons =
remember { SearchActionIcon.values().filter { it != SearchActionIcon.Custom } }
remember { SearchActionIcon.entries.filter { it != SearchActionIcon.Custom } }
Column(
modifier = Modifier.padding(paddingValues)
@ -787,7 +749,7 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
}
}
TextButton(
modifier = Modifier.padding(top = 16.dp, bottom = 24.dp),
modifier = Modifier.padding(vertical = 8.dp),
onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_custom_icon))
}
@ -795,7 +757,8 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(paddingValues)
modifier = Modifier.padding(paddingValues).fillMaxWidth()
) {
SearchActionIconTile {
SearchActionIcon(builder = action!!, size = 24.dp)
@ -817,15 +780,17 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
Row(
modifier = Modifier
.padding(top = 24.dp)
.horizontalScroll(rememberScrollState())
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.End
)
) {
OutlinedButton(
modifier = Modifier.padding(start = 16.dp),
onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_replace_icon))
}
OutlinedButton(
modifier = Modifier.padding(start = 16.dp),
onClick = { viewModel.setIcon(SearchActionIcon.Search) },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error

View File

@ -289,9 +289,6 @@ fun SearchActionsSettingsScreen() {
},
onDismiss = {
viewModel.dismissDialogs()
},
onDelete = {
viewModel.removeAction(editAction!!)
}
)
}

View File

@ -732,6 +732,7 @@
<string name="tag_exists_error">A tag with this name already exists.</string>
<string name="tag_exists_message">A tag with this name already exists. If you continue, the two tags will be merged.</string>
<string name="tag_no_items_message">No items are assigned to this tag. If you continue, the tag will be deleted.</string>
<string name="tag_empty_name">A tag cannot exist without a name. If you continue, the tag will be deleted.</string>
<string name="tag_select_items">Select items:</string>
<string name="tag_name">Tag name</string>
<plurals name="tag_selected_items">