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.animation.animateContentSize
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@ -22,7 +23,6 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -79,7 +79,7 @@ fun IconPicker(
var showIconPackFilter by remember { mutableStateOf(false) } var showIconPackFilter by remember { mutableStateOf(false) }
val installedIconPacks by viewModel.installedIconPacks.collectAsState(null) val installedIconPacks by viewModel.installedIconPacks.collectAsState(null)
val noPacksInstalled = installedIconPacks?.isEmpty() == true val packsInstalled = installedIconPacks?.isEmpty() == false
val columns = LocalGridSettings.current.columnCount val columns = LocalGridSettings.current.columnCount
@ -88,42 +88,38 @@ fun IconPicker(
columns = GridCells.Fixed(columns), columns = GridCells.Fixed(columns),
contentPadding = contentPadding, contentPadding = contentPadding,
) { ) {
if (packsInstalled) {
item(span = { GridItemSpan(columns) }) { item(span = { GridItemSpan(columns) }) {
SearchBar( SearchBar(
modifier = Modifier.padding(bottom = 16.dp), windowInsets = WindowInsets(0.dp),
expanded = false, expanded = false,
onExpandedChange = {}, onExpandedChange = {},
inputField = { inputField = {
SearchBarDefaults.InputField( SearchBarDefaults.InputField(
enabled = !noPacksInstalled, leadingIcon = {
leadingIcon = { Icon(
Icon( imageVector = Icons.Rounded.Search,
imageVector = Icons.Rounded.Search, contentDescription = null
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
) )
) },
}, onSearch = {},
query = query, expanded = false,
onQueryChange = { onExpandedChange = {},
query = it placeholder = {
scope.launch { Text(stringResource(R.string.icon_picker_search_icon))
viewModel.searchIcon(query, filterIconPack) },
} 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()) { if (suggestions.isNotEmpty()) {
item(span = { GridItemSpan(columns) }) {
Separator(stringResource(R.string.icon_picker_suggestions))
}
items(suggestions) { items(suggestions) {
IconPreview( IconPreview(
it, it,
@ -152,87 +148,82 @@ fun IconPicker(
} }
} }
} else { } else {
item(span = { GridItemSpan(columns) }) {
if (!installedIconPacks.isNullOrEmpty()) { Button(
item( onClick = { showIconPackFilter = !showIconPackFilter },
span = { GridItemSpan(columns) }, modifier = Modifier
.wrapContentWidth(align = Alignment.CenterHorizontally)
.padding(16.dp),
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 8.dp
)
) { ) {
Button( if (filterIconPack == null) {
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()
)
Icon( Icon(
Icons.Rounded.ArrowDropDown,
modifier = Modifier modifier = Modifier
.padding(start = ButtonDefaults.IconSpacing) .padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize), .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 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.DarkMode
import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material.icons.rounded.ErrorOutline
@ -69,21 +67,7 @@ fun ImportThemeSheet(
val error by viewModel.error val error by viewModel.error
var apply by viewModel.apply var apply by viewModel.apply
BottomSheetDialog( BottomSheetDialog(onDismiss) {
onDismissRequest = onDismiss,
confirmButton = if (theme != null && !error) {
{
Button(
onClick = {
viewModel.import()
onDismiss()
}
) {
Text(stringResource(R.string.action_import))
}
}
} else null,
) {
if (theme == null && !error) { if (theme == null && !error) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -111,8 +95,8 @@ fun ImportThemeSheet(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()) .padding(it),
.padding(it) horizontalAlignment = Alignment.End
) { ) {
ThemePreview( ThemePreview(
theme!!, theme!!,
@ -132,6 +116,15 @@ fun ImportThemeSheet(
apply = it 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.net.Uri
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.verticalScroll 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.Icons
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material3.* 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -37,35 +47,12 @@ fun RestoreBackupSheet(
val state by viewModel.state val state by viewModel.state
val compatibility by viewModel.compatibility val compatibility by viewModel.compatibility
BottomSheetDialog( BottomSheetDialog(onDismissRequest) {
onDismissRequest = onDismissRequest, Column (
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(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .padding(it),
.verticalScroll(rememberScrollState()) horizontalAlignment = Alignment.End,
.padding(it)
) { ) {
when (state) { when (state) {
RestoreBackupState.Parsing -> { 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( fun SearchablePicker(
value: SavableSearchable?, value: SavableSearchable?,
onValueChanged: (SavableSearchable?) -> Unit, onValueChanged: (SavableSearchable?) -> Unit,
title: @Composable () -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
val viewModel: SearchablePickerVM = viewModel() val viewModel: SearchablePickerVM = viewModel()
BottomSheetDialog(onDismissRequest = onDismissRequest, title = title) { BottomSheetDialog(onDismissRequest = onDismissRequest) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()

View File

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

View File

@ -1,374 +1,42 @@
package de.mm20.launcher2.ui.component 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.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.PaddingValues
import androidx.compose.foundation.layout.Row 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.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.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.ModalBottomSheet
import androidx.compose.foundation.layout.width import androidx.compose.material3.SheetState
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.rememberModalBottomSheetState
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.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier 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.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 @Composable
fun BottomSheetDialog( fun BottomSheetDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
title: (@Composable () -> Unit)? = null, footerItems: @Composable (() -> Unit)? = null,
actions: (@Composable RowScope.() -> Unit)? = null, bottomSheetState: SheetState = rememberModalBottomSheetState(),
confirmButton: @Composable (() -> Unit)? = null,
dismissButton: @Composable (() -> Unit)? = null,
dismissible: () -> Boolean = { true },
zIndex: Float = LocalZIndex.current + 1f,
content: @Composable (paddingValues: PaddingValues) -> Unit, content: @Composable (paddingValues: PaddingValues) -> Unit,
) { ) {
val scope = rememberCoroutineScope() ModalBottomSheet(
sheetState = bottomSheetState,
val focusManager = LocalFocusManager.current onDismissRequest = onDismissRequest,
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,
) { ) {
Overlay(zIndex = zIndex) { content(PaddingValues(horizontal = 24.dp, vertical = 8.dp))
BoxWithConstraints( if (footerItems != null) {
Row(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.systemBarsPadding() .padding(start = 24.dp, end = 24.dp, bottom = 8.dp),
.imePadding(), horizontalArrangement = Arrangement.spacedBy(
propagateMinConstraints = true, space = 16.dp,
contentAlignment = Alignment.BottomCenter alignment = Alignment.End,
)
) { ) {
val maxHeight = maxHeight footerItems()
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()
}
}
}
}
}
} }
} }
} }
}
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, onWidgetUpdated: (Widget) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
BottomSheetDialog(onDismissRequest = onDismiss, 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))
)
}
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View File

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

View File

@ -39,7 +39,6 @@ import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
@ -106,28 +105,7 @@ fun EditFavoritesSheet(
val loading by viewModel.loading val loading by viewModel.loading
val createShortcutTarget by viewModel.createShortcutTarget val createShortcutTarget by viewModel.createShortcutTarget
BottomSheetDialog( BottomSheetDialog(onDismiss) {
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
) {
if (loading) { if (loading) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@ -86,6 +86,8 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
.map { Tag(it) } .map { Tag(it) }
this.pinnedTags.value = pinnedTags this.pinnedTags.value = pinnedTags
createShortcutTarget.value = null
buildItemList() buildItemList()
loading.value = false 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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset 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.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -49,6 +51,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@ -77,8 +80,6 @@ fun EditTagSheet(
) { ) {
val viewModel: EditTagSheetVM = viewModel() val viewModel: EditTagSheetVM = viewModel()
val isCreatingNewTag = tag == null
val density = LocalDensity.current val density = LocalDensity.current
LaunchedEffect(tag) { LaunchedEffect(tag) {
@ -88,48 +89,14 @@ fun EditTagSheet(
if (viewModel.loading) return if (viewModel.loading) return
BottomSheetDialog( BottomSheetDialog(
title = { bottomSheetState = rememberModalBottomSheetState(true),
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))
}
}
},
onDismissRequest = { onDismissRequest = {
if (viewModel.page == EditTagSheetPage.CustomizeTag) { if (viewModel.page == EditTagSheetPage.CustomizeTag) {
viewModel.save() viewModel.save()
onTagSaved(viewModel.tagName) onTagSaved(viewModel.tagName)
} }
onDismiss() onDismiss()
}, }
dismissible = {
!(!isCreatingNewTag && viewModel.page == EditTagSheetPage.PickItems)
},
) { ) {
when (viewModel.page) { when (viewModel.page) {
EditTagSheetPage.CreateTag -> CreateNewTagPage(viewModel, it) EditTagSheetPage.CreateTag -> CreateNewTagPage(viewModel, it)
@ -161,55 +128,91 @@ fun CreateNewTagPage(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
value = viewModel.tagName, value = viewModel.tagName,
onValueChange = { viewModel.tagName = it } 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 @Composable
fun PickItems(viewModel: EditTagSheetVM, paddingValues: PaddingValues) { fun PickItems(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
val columns = LocalGridSettings.current.columnCount - 1 val columns = LocalGridSettings.current.columnCount - 1
LazyVerticalGrid(
modifier = Modifier.fillMaxWidth(), Scaffold (
columns = GridCells.Fixed(columns), contentWindowInsets = WindowInsets(0.dp),
contentPadding = paddingValues, 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) }) { LazyVerticalGrid(
Text( modifier = Modifier.fillMaxWidth(),
stringResource(id = R.string.tag_select_items), columns = GridCells.Fixed(columns),
style = MaterialTheme.typography.labelMedium, contentPadding = it
color = MaterialTheme.colorScheme.secondary, ) {
modifier = Modifier.padding(bottom = 8.dp) item(span = { GridItemSpan(columns) }) {
) Text(
} stringResource(id = R.string.tag_select_items),
items(viewModel.taggableApps) { style = MaterialTheme.typography.labelMedium,
val iconSize = 32.dp.toPixels() color = MaterialTheme.colorScheme.secondary,
val icon by remember(it.item.key) { modifier = Modifier.padding(bottom = 8.dp)
viewModel.getIcon(it.item, iconSize.toInt()) )
}.collectAsState(null) }
ListItem(item = it, icon = icon, onTagChanged = { tagged -> items(viewModel.taggableApps) {
if (tagged) viewModel.tagItem(it.item) val iconSize = 32.dp.toPixels()
else viewModel.untagItem(it.item) 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) }) { if (viewModel.taggableOther.isNotEmpty()) {
Box( item(span = { GridItemSpan(columns) }) {
modifier = Modifier Box(
.padding(vertical = 8.dp) modifier = Modifier
.background(MaterialTheme.colorScheme.outlineVariant) .padding(vertical = 8.dp)
.fillMaxWidth() .background(MaterialTheme.colorScheme.outlineVariant)
.height(1.dp) .fillMaxWidth()
) .height(1.dp)
} )
}
items(viewModel.taggableOther) { items(viewModel.taggableOther) {
val iconSize = 32.dp.toPixels() val iconSize = 32.dp.toPixels()
val icon by remember(it.item.key) { val icon by remember(it.item.key) {
viewModel.getIcon(it.item, iconSize.toInt()) viewModel.getIcon(it.item, iconSize.toInt())
}.collectAsState(null) }.collectAsState(null)
ListItem(item = it, icon = icon, onTagChanged = { tagged -> ListItem(item = it, icon = icon, onTagChanged = { tagged ->
if (tagged) viewModel.tagItem(it.item) if (tagged) viewModel.tagItem(it.item)
else viewModel.untagItem(it.item) else viewModel.untagItem(it.item)
}) })
}
}
} }
} }
} }
@ -284,7 +287,7 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
.clickable { .clickable {
viewModel.openIconPicker() viewModel.openIconPicker()
} }
.size(56.dp) .size(72.dp)
then ( then (
if (tagIcon != null) { if (tagIcon != null) {
Modifier Modifier
@ -327,7 +330,7 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
OutlinedTextField( OutlinedTextField(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
singleLine = true, singleLine = true,
placeholder = { Text(stringResource(R.string.tag_name)) }, label = { Text(stringResource(R.string.tag_name)) },
value = viewModel.tagName, value = viewModel.tagName,
onValueChange = { viewModel.tagName = it }, 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( SmallMessage(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
icon = Icons.Rounded.Warning, icon = Icons.Rounded.Warning,
text = stringResource( 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.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -83,7 +81,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
val oldName = oldTagName val oldName = oldTagName
val newName = tagName val newName = tagName
val tagIcon = tagCustomIcon 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 if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
else tagService.createTag(tagName, taggedItems) else tagService.createTag(tagName, taggedItems)

View File

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

View File

@ -3,21 +3,9 @@ package de.mm20.launcher2.ui.launcher.sheets
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll 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.runtime.Composable
import androidx.compose.ui.Modifier 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.search.SavableSearchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
@ -26,27 +14,7 @@ fun HiddenItemsSheet(
items: List<SavableSearchable>, items: List<SavableSearchable>,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val viewModel: HiddenItemsSheetVM = viewModel() BottomSheetDialog(onDismiss) {
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)
}
},
) {
SearchResultGrid( SearchResultGrid(
items, items,
modifier = Modifier modifier = Modifier

View File

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

View File

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.AccessTime
import androidx.compose.material.icons.rounded.Alarm import androidx.compose.material.icons.rounded.Alarm
import androidx.compose.material.icons.rounded.AlignVerticalBottom import androidx.compose.material.icons.rounded.AlignVerticalBottom
@ -370,11 +371,16 @@ fun ConfigureClockWidgetSheet(
icon = { icon = {
SegmentedButtonDefaults.Icon( SegmentedButtonDefaults.Icon(
active = compact == false, active = compact == false,
activeContent = {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
)
}
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.HorizontalSplit, imageVector = Icons.Rounded.HorizontalSplit,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(SegmentedButtonDefaults.IconSize)
) )
} }
} }
@ -390,11 +396,16 @@ fun ConfigureClockWidgetSheet(
icon = { icon = {
SegmentedButtonDefaults.Icon( SegmentedButtonDefaults.Icon(
active = compact == true, active = compact == true,
activeContent = {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = null,
)
}
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.VerticalSplit, imageVector = Icons.Rounded.VerticalSplit,
contentDescription = null, 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.AnimatedVisibility
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -43,11 +42,13 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -57,7 +58,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -398,39 +398,11 @@ fun NoteWidgetConflictResolveSheet(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
var selectedStrategy by remember { mutableStateOf<LinkedFileConflictStrategy?>(null) } var selectedStrategy by remember { mutableStateOf<LinkedFileConflictStrategy?>(null) }
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
BottomSheetDialog( BottomSheetDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { bottomSheetState = bottomSheetState
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))
}
}
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -463,6 +435,40 @@ fun NoteWidgetConflictResolveSheet(
selected = selectedStrategy == LinkedFileConflictStrategy.KeepFile, selected = selectedStrategy == LinkedFileConflictStrategy.KeepFile,
onSelect = { 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), .padding(16.dp),
text = content, onTextChange = {} 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( IconButton(
modifier = Modifier.align(Alignment.TopEnd), modifier = Modifier.align(Alignment.TopEnd),
onClick = onSelect onClick = onSelect

View File

@ -2,26 +2,24 @@ package de.mm20.launcher2.ui.settings.backup
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.CheckCircleOutline
import androidx.compose.material3.* import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.LargeMessage import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.SmallMessage
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -50,68 +48,29 @@ fun CreateBackupSheet(
val state by viewModel.state val state by viewModel.state
BottomSheetDialog( if (state == CreateBackupState.BackingUp || state == CreateBackupState.BackedUp) {
onDismissRequest = onDismissRequest, BottomSheetDialog(onDismissRequest) {
title = { if (state == CreateBackupState.BackingUp) {
Text( Box(
stringResource(id = R.string.preference_backup), modifier = Modifier
) .fillMaxWidth()
}, .aspectRatio(1f),
confirmButton = { contentAlignment = Alignment.Center
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
) { ) {
Text(stringResource(R.string.close)) CircularProgressIndicator(
} modifier = Modifier.size(48.dp)
}
},
) {
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
)
) )
} }
} }
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)) { if (!isOpenSearch && value is GestureAction.Launch && (showAppPicker || app == null)) {
SearchablePicker( SearchablePicker(
title = { Text(title) },
onDismissRequest = { onDismissRequest = {
showAppPicker = false showAppPicker = false
if (app == null) onValueChanged(GestureAction.NoAction) 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.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues 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.Add
import androidx.compose.material.icons.rounded.Android import androidx.compose.material.icons.rounded.Android
import androidx.compose.material.icons.rounded.ArrowDropDown 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.ManageSearch
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RemoveCircleOutline import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material.icons.rounded.ToggleOn import androidx.compose.material.icons.rounded.ToggleOn
import androidx.compose.material.icons.rounded.TravelExplore 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.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState 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.actions.SearchActionIcon
import de.mm20.launcher2.searchactions.builders.AppSearchActionBuilder import de.mm20.launcher2.searchactions.builders.AppSearchActionBuilder
import de.mm20.launcher2.searchactions.builders.CustomIntentActionBuilder 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.CustomWebsearchActionBuilder
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ExperimentalBadge import de.mm20.launcher2.ui.component.ExperimentalBadge
@ -99,13 +97,11 @@ fun EditSearchActionSheet(
initialSearchAction: CustomizableSearchActionBuilder?, initialSearchAction: CustomizableSearchActionBuilder?,
onSave: (CustomizableSearchActionBuilder) -> Unit, onSave: (CustomizableSearchActionBuilder) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onDelete: () -> Unit = {},
) { ) {
val viewModel: EditSearchActionSheetVM = viewModel() val viewModel: EditSearchActionSheetVM = viewModel()
LaunchedEffect(initialSearchAction) { LaunchedEffect(initialSearchAction) {
viewModel.init(initialSearchAction) viewModel.init(initialSearchAction)
} }
val createNew by viewModel.createNew
val page by viewModel.currentPage val page by viewModel.currentPage
val searchAction by viewModel.searchAction val searchAction by viewModel.searchAction
@ -114,10 +110,7 @@ fun EditSearchActionSheet(
viewModel.onDismiss() viewModel.onDismiss()
onDismiss() onDismiss()
}, },
dismissible = { footerItems = when (page) {
page != EditSearchActionPage.PickIcon
},
confirmButton = when (page) {
EditSearchActionPage.CustomizeAppSearch, EditSearchActionPage.CustomizeAppSearch,
EditSearchActionPage.CustomizeWebSearch, EditSearchActionPage.CustomizeWebSearch,
EditSearchActionPage.CustomizeCustomIntent -> { EditSearchActionPage.CustomizeCustomIntent -> {
@ -170,37 +163,6 @@ fun EditSearchActionSheet(
} }
else -> null 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) { when (page) {
EditSearchActionPage.SelectType -> SelectTypePage(viewModel, it) EditSearchActionPage.SelectType -> SelectTypePage(viewModel, it)
@ -741,7 +703,7 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
if (action?.customIcon == null) { if (action?.customIcon == null) {
val availableIcons = val availableIcons =
remember { SearchActionIcon.values().filter { it != SearchActionIcon.Custom } } remember { SearchActionIcon.entries.filter { it != SearchActionIcon.Custom } }
Column( Column(
modifier = Modifier.padding(paddingValues) modifier = Modifier.padding(paddingValues)
@ -787,7 +749,7 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
} }
} }
TextButton( TextButton(
modifier = Modifier.padding(top = 16.dp, bottom = 24.dp), modifier = Modifier.padding(vertical = 8.dp),
onClick = { pickIconLauncher.launch("image/*") }) { onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_custom_icon)) Text(stringResource(R.string.websearch_dialog_custom_icon))
} }
@ -795,7 +757,8 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
} else { } else {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(paddingValues) modifier = Modifier.padding(paddingValues).fillMaxWidth()
) { ) {
SearchActionIconTile { SearchActionIconTile {
SearchActionIcon(builder = action!!, size = 24.dp) SearchActionIcon(builder = action!!, size = 24.dp)
@ -817,15 +780,17 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
Row( Row(
modifier = Modifier modifier = Modifier
.padding(top = 24.dp) .padding(top = 24.dp)
.horizontalScroll(rememberScrollState()) .horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.End
)
) { ) {
OutlinedButton( OutlinedButton(
modifier = Modifier.padding(start = 16.dp),
onClick = { pickIconLauncher.launch("image/*") }) { onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_replace_icon)) Text(stringResource(R.string.websearch_dialog_replace_icon))
} }
OutlinedButton( OutlinedButton(
modifier = Modifier.padding(start = 16.dp),
onClick = { viewModel.setIcon(SearchActionIcon.Search) }, onClick = { viewModel.setIcon(SearchActionIcon.Search) },
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error contentColor = MaterialTheme.colorScheme.error

View File

@ -289,9 +289,6 @@ fun SearchActionsSettingsScreen() {
}, },
onDismiss = { onDismiss = {
viewModel.dismissDialogs() 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_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_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_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_select_items">Select items:</string>
<string name="tag_name">Tag name</string> <string name="tag_name">Tag name</string>
<plurals name="tag_selected_items"> <plurals name="tag_selected_items">