[WIP] reimplement bottom sheets

Fix #1725
This commit is contained in:
MM20
2026-01-20 23:09:29 +01:00
parent 7536ae76ad
commit 0c2782b35a
40 changed files with 1315 additions and 678 deletions

View File

@@ -2,15 +2,22 @@ package de.mm20.launcher2.ui.common
import android.net.Uri
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.FlexibleBottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -19,138 +26,160 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.backup.BackupCompatibility
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.BottomSheet
import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.SmallMessage
@Composable
fun RestoreBackupSheet(
uri: Uri,
uri: Uri?,
onDismissRequest: () -> Unit
) {
val viewModel: RestoreBackupSheetVM = viewModel()
BottomSheet(
expanded = uri != null,
onDismissRequest = onDismissRequest,
) {
LaunchedEffect(uri) {
viewModel.setInputUri(uri)
}
val viewModel: RestoreBackupSheetVM = viewModel()
val state by viewModel.state
val compatibility by viewModel.compatibility
if (uri != null) {
LaunchedEffect(uri) {
viewModel.setInputUri(uri)
}
}
val state by viewModel.state
val compatibility by viewModel.compatibility
BottomSheetDialog(onDismissRequest) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(it),
horizontalAlignment = Alignment.End,
.wrapContentHeight(),
) {
when (state) {
RestoreBackupState.Parsing -> {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentAlignment = Alignment.Center
CenterAlignedTopAppBar(
title = {
Text(stringResource(R.string.preference_restore))
},
actions = {
FilledTonalIconButton(
onClick = onDismissRequest
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
)
Icon(painterResource(R.drawable.close_24px), stringResource(R.string.close))
}
}
RestoreBackupState.InvalidFile -> {
LargeMessage(
modifier = Modifier.aspectRatio(1f),
icon = R.drawable.error_48px,
text = stringResource(id = R.string.restore_invalid_file)
)
}
RestoreBackupState.Ready -> {
val metadata by viewModel.metadata
if (metadata != null) {
Column {
SmallMessage(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(bottom = 16.dp),
icon = R.drawable.info_24px,
text = stringResource(
R.string.restore_meta,
DateUtils.formatDateTime(
LocalContext.current,
metadata!!.timestamp,
DateUtils.FORMAT_ABBREV_ALL or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR
),
metadata!!.deviceName,
stringResource(R.string.app_name) + " " + metadata!!.appVersionName,
)
)
AnimatedContent(
state,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
when (it) {
RestoreBackupState.Parsing, RestoreBackupState.Restoring -> {
Box(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
)
if (compatibility == BackupCompatibility.Incompatible) {
LargeMessage(
modifier = Modifier.aspectRatio(1f),
icon = R.drawable.error_48px,
}
}
RestoreBackupState.InvalidFile -> {
LargeMessage(
modifier = Modifier.navigationBarsPadding(),
icon = R.drawable.error_48px,
text = stringResource(id = R.string.restore_invalid_file)
)
}
RestoreBackupState.Ready -> {
val metadata by viewModel.metadata
if (metadata != null) {
Column(
modifier = Modifier.fillMaxWidth()
) {
SmallMessage(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.wrapContentHeight(),
icon = R.drawable.info_24px,
text = stringResource(
id = R.string.restore_incompatible_file,
stringResource(R.string.app_name)
R.string.restore_meta,
DateUtils.formatDateTime(
LocalContext.current,
metadata!!.timestamp,
DateUtils.FORMAT_ABBREV_ALL or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR
),
metadata!!.deviceName,
stringResource(R.string.app_name) + " " + metadata!!.appVersionName,
)
)
} else {
if (compatibility == BackupCompatibility.PartiallyCompatible) {
SmallMessage(
color = MaterialTheme.colorScheme.secondary,
if (compatibility == BackupCompatibility.Incompatible) {
LargeMessage(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
icon = R.drawable.warning_24px,
text =
stringResource(
R.string.restore_different_minor_version,
stringResource(R.string.app_name)
)
.navigationBarsPadding(),
icon = R.drawable.error_48px,
text = stringResource(
id = R.string.restore_incompatible_file,
stringResource(R.string.app_name)
)
)
} else {
if (compatibility == BackupCompatibility.PartiallyCompatible) {
SmallMessage(
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding(),
icon = R.drawable.warning_24px,
text =
stringResource(
R.string.restore_different_minor_version,
stringResource(R.string.app_name)
)
)
}
}
}
}
}
}
RestoreBackupState.Restoring -> {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
RestoreBackupState.Restored -> {
LargeMessage(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding(),
icon = R.drawable.check_circle_48px,
text = stringResource(
id = R.string.restore_complete
)
)
}
}
RestoreBackupState.Restored -> {
LargeMessage(
modifier = Modifier.aspectRatio(1f),
icon = R.drawable.check_circle_48px,
text = stringResource(
id = R.string.restore_complete
)
)
}
}
if (state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
Button(
onClick = { viewModel.restore() }
AnimatedVisibility(state == RestoreBackupState.Ready && compatibility != BackupCompatibility.Incompatible) {
FlexibleBottomAppBar(
horizontalArrangement = Arrangement.End,
) {
Text(stringResource(R.string.preference_restore))
Button(
onClick = { viewModel.restore() },
modifier = Modifier.navigationBarsPadding(),
) {
Text(stringResource(R.string.preference_restore))
}
}
}
}

View File

@@ -26,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.toPixels
@@ -38,7 +38,7 @@ fun SearchablePicker(
) {
val viewModel: SearchablePickerVM = viewModel()
BottomSheetDialog(onDismissRequest = onDismissRequest) {
DismissableBottomSheet(onDismissRequest = onDismissRequest) {
Column(
modifier = Modifier
.fillMaxSize()

View File

@@ -2,13 +2,20 @@ package de.mm20.launcher2.ui.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.Icon
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
@@ -21,32 +28,43 @@ 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.LocalDensity
import androidx.compose.ui.res.painterResource
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.DismissableBottomSheet
import de.mm20.launcher2.ui.component.SmallMessage
import kotlinx.coroutines.launch
@Composable
fun WeatherLocationSearchDialog(
expanded: Boolean,
onDismissRequest: () -> Unit
) {
val scope = rememberCoroutineScope()
val viewModel: WeatherLocationSearchDialogVM = viewModel()
val isSearching by viewModel.isSearchingLocation
val locations by viewModel.locationResults
BottomSheetDialog(onDismissRequest) {
DismissableBottomSheet(
expanded = expanded,
onDismissRequest = onDismissRequest
) {
val scope = rememberCoroutineScope()
val viewModel: WeatherLocationSearchDialogVM = viewModel()
val isSearching by viewModel.isSearchingLocation
val locations by viewModel.locationResults
var query by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize()
) {
SearchBar(
modifier = Modifier.padding(bottom = 8.dp),
windowInsets = WindowInsets(0.dp),
DockedSearchBar(
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 8.dp,
start = 16.dp,
end = 16.dp
),
expanded = false,
onExpandedChange = {},
inputField = {
@@ -74,11 +92,13 @@ fun WeatherLocationSearchDialog(
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
.navigationBarsPadding()
)
} else if (locations.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = it
contentPadding = WindowInsets.navigationBars.asPaddingValues()
) {
items(locations) {
Text(
@@ -89,16 +109,15 @@ fun WeatherLocationSearchDialog(
viewModel.setLocation(it)
onDismissRequest()
}
.padding(
vertical = 16.dp
)
.padding(16.dp)
)
}
}
} else if (query.isNotEmpty()) {
SmallMessage(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth()
.padding(16.dp),
icon = R.drawable.error_24px,
text = stringResource(R.string.weather_location_search_no_result)
)

View File

@@ -20,16 +20,16 @@ class WeatherLocationSearchDialogVM: ViewModel(), KoinComponent {
private var debounceSearchJob: Job? = null
suspend fun searchLocation(query: String) {
debounceSearchJob?.cancelAndJoin()
if (query.isBlank()) {
locationResults.value = emptyList()
isSearchingLocation.value = false
return
}
debounceSearchJob?.cancelAndJoin()
withContext(coroutineContext) {
debounceSearchJob = launch {
delay(1000)
isSearchingLocation.value = true
delay(1000)
locationResults.value = repository.searchLocations(query).first()
isSearchingLocation.value = false
}

View File

@@ -0,0 +1,159 @@
package de.mm20.launcher2.ui.component
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.overlays.Overlay
import kotlinx.coroutines.CancellationException
@Composable
fun BottomSheet(
expanded: Boolean,
onDismissRequest: () -> Unit = {},
content: @Composable () -> Unit,
) {
BottomSheet(
state = expanded,
expanded = { it },
onDismissRequest = onDismissRequest,
) {
content()
}
}
/**
* A non-dismissable bottom sheet.
*/
@Composable
fun <T>BottomSheet(
state: T,
expanded: (T) -> Boolean,
onDismissRequest: () -> Unit,
content: @Composable (state: T) -> Unit,
) {
val expandedState = remember { MutableTransitionState(state) }
expandedState.targetState = state
val expandedCurrent = expanded(expandedState.currentState)
val expandedTarget = expanded(expandedState.targetState)
if (expandedCurrent || expandedTarget) {
Overlay {
val backProgress = remember { Animatable(0f) }
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
focusManager.clearFocus(true)
}
PredictiveBackHandler {
try {
it.collect {
backProgress.snapTo(it.progress)
}
onDismissRequest()
backProgress.animateTo(0f)
} catch (_: CancellationException) {
backProgress.animateTo(0f)
}
}
val transition = rememberTransition(expandedState)
val scrimColor by transition.animateColor(
transitionSpec = { MaterialTheme.motionScheme.slowEffectsSpec() },
) { if (expanded(it)) BottomSheetDefaults.ScrimColor else Color.Transparent }
Box(
modifier = Modifier
.fillMaxSize()
.background(scrimColor)
.graphicsLayer {
scaleX = 1f - backProgress.value * 0.1f
scaleY = 1f - backProgress.value * 0.1f
transformOrigin = TransformOrigin(0.5f, 1f)
}
.imePadding()
) {
Box(Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures {
onDismissRequest()
}
}
)
val focusRequester = remember { FocusRequester() }
if (expanded(transition.currentState)) {
DisposableEffect(Unit) {
focusRequester.requestFocus()
onDispose {
focusRequester.freeFocus()
}
}
}
transition.AnimatedVisibility(
visible = { expanded(it) },
modifier = Modifier.align(Alignment.BottomCenter),
enter = slideInVertically { it },
exit = slideOutVertically { it }
) {
Surface(
shadowElevation = 1.dp,
shape = BottomSheetDefaults.ExpandedShape,
modifier = Modifier
.statusBarsPadding()
.imePadding()
.fillMaxWidth()
.widthIn(max = BottomSheetDefaults.SheetMaxWidth)
.align(Alignment.BottomCenter)
.wrapContentHeight()
) {
content(
if (expandedTarget) expandedState.targetState else expandedState.currentState
)
}
}
}
}
}
}

View File

@@ -1,39 +0,0 @@
package de.mm20.launcher2.ui.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.ktx.toDp
@Composable
fun BottomSheetDialog(
onDismissRequest: () -> Unit,
bottomSheetState: SheetState = rememberModalBottomSheetState(),
windowInsets: WindowInsets = WindowInsets(left = 24.dp, right = 24.dp),
content: @Composable (paddingValues: PaddingValues) -> Unit,
) {
ModalBottomSheet(
modifier = Modifier
.statusBarsPadding()
.padding(top = 8.dp),
sheetState = bottomSheetState,
onDismissRequest = onDismissRequest,
contentWindowInsets = { windowInsets },
containerColor = BottomSheetDefaults.ContainerColor
) {
content(PaddingValues(
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current).toDp() + 8.dp,
))
}
}

View File

@@ -0,0 +1,346 @@
package de.mm20.launcher2.ui.component
import android.util.Log
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.rememberTransition
import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.gestures.detectTapGestures
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.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
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.Velocity
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.overlays.Overlay
import kotlinx.coroutines.CancellationException
@Composable
fun DismissableBottomSheet(
expanded: Boolean = true,
onDismissRequest: () -> Unit,
content: @Composable (contentPadding: PaddingValues) -> Unit,
) {
DismissableBottomSheet(
state = expanded,
expanded = { it },
onDismissRequest = onDismissRequest,
) {
content(PaddingValues())
}
}
/**
* A bottom sheet that can be dismissed by swiping down.
*
* Use `BottomSheet` for a sheet that can't be dismissed.
*
* @param expanded Whether the bottom sheet is visible.
* @param onDismissRequest Called when the bottom sheet is dismissed.
* @param content The content of the bottom sheet.
*/
@Composable
fun <T> DismissableBottomSheet(
state: T,
expanded: (T) -> Boolean,
onDismissRequest: () -> Unit,
content: @Composable (state: T) -> Unit,
) {
val expandedState = remember { MutableTransitionState(state) }
expandedState.targetState = state
val expandedCurrent = expanded(expandedState.currentState)
val expandedTarget = expanded(expandedState.targetState)
if (expandedCurrent || expandedTarget) {
Overlay {
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
focusManager.clearFocus(true)
}
val backProgress = remember { Animatable(0f) }
val draggableState = remember {
AnchoredDraggableState(SheetValue.Hidden, DraggableAnchors {
SheetValue.Hidden at 0f
})
}
PredictiveBackHandler {
try {
it.collect {
backProgress.snapTo(it.progress)
}
onDismissRequest()
} catch (_: CancellationException) {
backProgress.animateTo(1f)
}
}
val motionSpec = MaterialTheme.motionScheme.defaultEffectsSpec<Float>()
val transition = rememberTransition(expandedState)
val scrimColor by transition.animateColor(
transitionSpec = { MaterialTheme.motionScheme.slowEffectsSpec() },
) { if (expanded(it)) BottomSheetDefaults.ScrimColor else Color.Transparent }
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.background(scrimColor)
.graphicsLayer {
scaleX = 1f - backProgress.value * 0.1f
scaleY = 1f - backProgress.value * 0.1f
transformOrigin = TransformOrigin(0.5f, 1f)
}
.imePadding()
) {
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures {
onDismissRequest()
}
}
)
val focusRequester = remember { FocusRequester() }
if (draggableState.anchors.size > 1) {
LaunchedEffect(Unit) {
draggableState.animateTo(SheetValue.PartiallyExpanded, motionSpec)
}
if (draggableState.settledValue != SheetValue.Hidden) {
DisposableEffect(Unit) {
focusRequester.requestFocus()
onDispose {
focusRequester.freeFocus()
}
}
}
LaunchedEffect(expandedTarget) {
if (expandedTarget != expandedCurrent && draggableState.settledValue == draggableState.targetValue) {
if (!expandedTarget && draggableState.settledValue != SheetValue.Hidden) {
draggableState.animateTo(SheetValue.Hidden, motionSpec)
}
}
}
LaunchedEffect(draggableState.settledValue) {
if (draggableState.settledValue == draggableState.targetValue) {
if (draggableState.settledValue == SheetValue.Hidden) {
onDismissRequest()
}
}
}
}
val containerHeight = maxHeight.toPixels()
var sheetHeight by remember { mutableIntStateOf(0) }
val flingBehavior =
AnchoredDraggableDefaults.flingBehavior(
state = draggableState,
animationSpec = motionSpec,
)
Surface(
shadowElevation = 1.dp,
shape = BottomSheetDefaults.ExpandedShape,
color = MaterialTheme.colorScheme.surfaceContainerLow,
modifier = Modifier
.statusBarsPadding()
.focusRequester(focusRequester)
.nestedScroll(
remember(draggableState) {
BottomSheetNestedScrollConnection(
anchoredDraggableState = draggableState,
flingBehavior = flingBehavior,
)
}
)
.onSizeChanged {
sheetHeight = it.height
draggableState.updateAnchors(
DraggableAnchors {
SheetValue.Hidden at 0f
if (it.height > containerHeight * 0.5f) {
SheetValue.PartiallyExpanded at containerHeight * -0.5f
SheetValue.Expanded at it.height * -1f
} else {
SheetValue.PartiallyExpanded at it.height * -1f
}
},
)
}
.fillMaxWidth()
.widthIn(max = BottomSheetDefaults.SheetMaxWidth)
.align(Alignment.BottomCenter)
.wrapContentHeight()
.heightIn(max = maxHeight)
.graphicsLayer {
translationY = draggableState.offset.coerceAtMost(0f) + sheetHeight
}
.anchoredDraggable(
state = draggableState,
orientation = Orientation.Vertical,
flingBehavior = flingBehavior,
)
) {
CompositionLocalProvider(
LocalOverscrollFactory provides null
) {
Column(
modifier = Modifier
.fillMaxWidth()
.consumeWindowInsets(WindowInsets.ime),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BottomSheetDefaults.DragHandle()
Box {
content(
if (expandedTarget) expandedState.targetState else expandedState.currentState
)
}
}
}
}
}
}
}
}
private fun BottomSheetNestedScrollConnection(
anchoredDraggableState: AnchoredDraggableState<SheetValue>,
flingBehavior: FlingBehavior,
): NestedScrollConnection =
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
return if (delta < 0 && source == NestedScrollSource.UserInput) {
anchoredDraggableState.dispatchRawDelta(delta).toOffset()
} else {
Offset.Zero
}
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
return if (source == NestedScrollSource.UserInput) {
anchoredDraggableState.dispatchRawDelta(available.y).toOffset()
} else {
Offset.Zero
}
}
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = available.y
val currentOffset = anchoredDraggableState.requireOffset()
val minAnchor = anchoredDraggableState.anchors.minPosition()
return if (toFling < 0 && currentOffset > minAnchor) {
anchoredDraggableState.anchoredDrag(flingBehavior, 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 {
val toFling = available.y
val consumedByAnchoredDraggableFling =
anchoredDraggableState.anchoredDrag(flingBehavior, toFling)
return Velocity(consumed.x, consumedByAnchoredDraggableFling)
}
private fun Float.toOffset(): Offset =
Offset(x = 0f, y = this)
private fun AnchoredDraggableState<SheetValue>.newOffsetForDelta(delta: Float) =
((if (offset.isNaN()) 0f else offset) + delta).coerceIn(
anchoredDraggableState.anchors.minPosition(),
anchoredDraggableState.anchors.maxPosition(),
)
private suspend fun AnchoredDraggableState<SheetValue>.anchoredDrag(
flingBehavior: FlingBehavior,
initialVelocity: Float
): Float {
var consumedVelocity = 0f
anchoredDraggableState.anchoredDrag {
val scrollScope =
object : ScrollScope {
override fun scrollBy(pixels: Float): Float {
val newOffset = newOffsetForDelta(pixels)
val consumed = newOffset - offset
dragTo(newOffset)
return consumed
}
}
consumedVelocity = with(flingBehavior) { scrollScope.performFling(initialVelocity) }
}
return consumedVelocity
}
}

View File

@@ -355,11 +355,10 @@ fun SearchColumn(
val sheetManager = LocalBottomSheetManager.current
if (sheetManager.hiddenItemsSheetShown.value) {
HiddenItemsSheet(
items = hiddenResults,
onDismiss = { sheetManager.dismissHiddenItemsSheet() })
}
HiddenItemsSheet(
expanded = sheetManager.hiddenItemsSheetShown.value,
items = hiddenResults,
onDismiss = { sheetManager.dismissHiddenItemsSheet() })
}

View File

@@ -26,10 +26,11 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
@@ -97,7 +98,7 @@ import de.mm20.launcher2.search.calendar.CalendarListType
import de.mm20.launcher2.themes.colors.atTone
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalAppWidgetHost
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.DragResizeHandle
import de.mm20.launcher2.ui.component.LargeMessage
import de.mm20.launcher2.ui.component.MissingPermissionBanner
@@ -113,8 +114,8 @@ import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.ui.settings.SettingsActivity
import de.mm20.launcher2.widgets.AppWidget
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.AppsWidget
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.NotesWidget
import de.mm20.launcher2.widgets.WeatherWidget
@@ -129,19 +130,20 @@ import kotlin.math.roundToInt
@Composable
fun ConfigureWidgetSheet(
expanded: Boolean,
widget: Widget,
onWidgetUpdated: (Widget) -> Unit,
onDismiss: () -> Unit,
) {
BottomSheetDialog(
onDismissRequest = onDismiss,
windowInsets = WindowInsets()
DismissableBottomSheet(
expanded = expanded,
onDismissRequest = onDismiss
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = if (widget is AppWidget) 8.dp else 16.dp)
.padding(it)
.padding(horizontal = if (widget is AppWidget) 8.dp else 16.dp, vertical = 16.dp)
.navigationBarsPadding()
.verticalScroll(rememberScrollState())
) {
when (widget) {
@@ -648,28 +650,28 @@ fun ColumnScope.ConfigureAppWidget(
)
Text(stringResource(R.string.widget_action_replace))
}
if (replaceWidget) {
WidgetPickerSheet(
onDismiss = { replaceWidget = false },
onWidgetSelected = {
val updatedWidget = when (it) {
is AppWidget -> widget.copy(
config = widget.config.copy(
widgetId = it.config.widgetId
)
WidgetPickerSheet(
expanded = replaceWidget,
onDismiss = { replaceWidget = false },
onWidgetSelected = {
val updatedWidget = when (it) {
is AppWidget -> widget.copy(
config = widget.config.copy(
widgetId = it.config.widgetId
)
)
is WeatherWidget -> it.copy(id = widget.id)
is MusicWidget -> it.copy(id = widget.id)
is CalendarWidget -> it.copy(id = widget.id)
is AppsWidget -> it.copy(id = widget.id)
is NotesWidget -> it.copy(id = widget.id)
}
onWidgetUpdated(updatedWidget)
replaceWidget = false
is WeatherWidget -> it.copy(id = widget.id)
is MusicWidget -> it.copy(id = widget.id)
is CalendarWidget -> it.copy(id = widget.id)
is AppsWidget -> it.copy(id = widget.id)
is NotesWidget -> it.copy(id = widget.id)
}
)
}
onWidgetUpdated(updatedWidget)
replaceWidget = false
}
)
return
}

View File

@@ -1,10 +1,15 @@
package de.mm20.launcher2.ui.launcher.sheets
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
@@ -13,7 +18,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
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
@@ -21,10 +25,10 @@ import androidx.compose.runtime.collectAsState
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -39,27 +43,35 @@ import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.IconPicker
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.toPixels
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@Composable
fun CustomizeSearchableSheet(
searchable: SavableSearchable,
searchable: SavableSearchable?,
onDismiss: () -> Unit,
) {
val scope = rememberCoroutineScope()
val viewModel: CustomizeSearchableSheetVM =
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
val pickIcon by viewModel.isIconPickerOpen
DismissableBottomSheet(
state = searchable,
expanded = { it != null },
onDismissRequest = { onDismiss() }) { searchable ->
searchable ?: return@DismissableBottomSheet
val viewModel: CustomizeSearchableSheetVM =
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
val pickIcon by viewModel.isIconPickerOpen
BottomSheetDialog(onDismissRequest = { if (!pickIcon) onDismiss() }) {
Column(
modifier = Modifier.padding(it),
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(16.dp)
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val iconSize = 64.dp
@@ -261,19 +273,14 @@ fun CustomizeSearchableSheet(
}
}
if (pickIcon) {
val bottomSheetState = rememberModalBottomSheetState()
BottomSheetDialog(
DismissableBottomSheet(
onDismissRequest = { viewModel.closeIconPicker() },
bottomSheetState = bottomSheetState
) {
IconPicker(
searchable = searchable,
onSelect = {
scope.launch {
viewModel.pickIcon(it)
bottomSheetState.hide()
viewModel.closeIconPicker()
}
},
contentPadding = it,
)

View File

@@ -12,9 +12,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -56,6 +59,7 @@ import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
@@ -75,7 +79,7 @@ import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Tag
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.TagChip
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.dragndrop.DraggableItem
@@ -83,24 +87,27 @@ import de.mm20.launcher2.ui.component.dragndrop.LazyDragAndDropRow
import de.mm20.launcher2.ui.component.dragndrop.LazyVerticalDragAndDropGrid
import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropGridState
import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalGridSettings
import kotlin.math.roundToInt
@Composable
fun EditFavoritesSheet(
expanded: Boolean,
onDismiss: () -> Unit,
) {
val viewModel: EditFavoritesSheetVM = viewModel()
DismissableBottomSheet(expanded, onDismissRequest = onDismiss) {
LaunchedEffect(null) {
viewModel.reload()
}
val viewModel: EditFavoritesSheetVM = viewModel()
val loading by viewModel.loading
val createShortcutTarget by viewModel.createShortcutTarget
LaunchedEffect(null) {
viewModel.reload()
}
val loading by viewModel.loading
val createShortcutTarget by viewModel.createShortcutTarget
BottomSheetDialog(onDismiss) {
if (loading) {
Box(
modifier = Modifier
@@ -115,11 +122,18 @@ fun EditFavoritesSheet(
)
}
} else {
ReorderFavoritesGrid(viewModel, it)
if (createShortcutTarget != null) {
BottomSheetDialog({ viewModel.cancelPickShortcut() }) {
ShortcutPicker(viewModel, it)
}
ReorderFavoritesGrid(
viewModel,
PaddingValues(
start = 16.dp,
end = 16.dp,
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
)
)
DismissableBottomSheet(
expanded = createShortcutTarget != null,
onDismissRequest = { viewModel.cancelPickShortcut() }) {
ShortcutPicker(viewModel, it)
}
}
}

View File

@@ -31,15 +31,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -65,7 +61,7 @@ import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.Tag
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.IconPicker
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.SmallMessage
import de.mm20.launcher2.ui.component.emojipicker.EmojiPicker
@@ -88,8 +84,7 @@ fun EditTagSheet(
if (viewModel.loading) return
BottomSheetDialog(
bottomSheetState = rememberModalBottomSheetState(true),
DismissableBottomSheet(
onDismissRequest = {
if (viewModel.page == EditTagSheetPage.CustomizeTag) {
viewModel.save()

View File

@@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher.sheets
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -10,13 +11,13 @@ 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.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.GestureAction
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.launcher.scaffold.Gesture
@@ -24,38 +25,43 @@ data class FailedGesture(val gesture: Gesture, val action: GestureAction)
@Composable
fun FailedGestureSheet(
failedGesture: FailedGesture,
failedGesture: FailedGesture?,
onDismiss: () -> Unit,
) {
val viewModel: FailedGestureSheetVM = viewModel()
val actionName = stringResource(when(failedGesture.action) {
is GestureAction.Search -> R.string.gesture_action_open_search
is GestureAction.Notifications -> R.string.gesture_action_notifications
is GestureAction.ScreenLock -> R.string.gesture_action_lock_screen
is GestureAction.QuickSettings -> R.string.gesture_action_quick_settings
is GestureAction.Recents -> R.string.gesture_action_recents
is GestureAction.PowerMenu -> R.string.gesture_action_power_menu
else -> R.string.gesture_action_none
})
val gestureName = stringResource(when(failedGesture.gesture) {
Gesture.DoubleTap -> R.string.preference_gesture_double_tap
Gesture.LongPress -> R.string.preference_gesture_long_press
Gesture.SwipeDown -> R.string.preference_gesture_swipe_down
Gesture.SwipeLeft -> R.string.preference_gesture_swipe_left
Gesture.SwipeRight -> R.string.preference_gesture_swipe_right
Gesture.SwipeUp -> R.string.preference_gesture_swipe_up
Gesture.HomeButton -> R.string.preference_gesture_home_button
else -> throw IllegalArgumentException("Unknown gesture: ${failedGesture.gesture}")
})
BottomSheetDialog(
DismissableBottomSheet(
state = failedGesture,
expanded = { it != null },
onDismissRequest = onDismiss,
) {
it ?: return@DismissableBottomSheet
val viewModel: FailedGestureSheetVM = viewModel()
val actionName = stringResource(when(it.action) {
is GestureAction.Search -> R.string.gesture_action_open_search
is GestureAction.Notifications -> R.string.gesture_action_notifications
is GestureAction.ScreenLock -> R.string.gesture_action_lock_screen
is GestureAction.QuickSettings -> R.string.gesture_action_quick_settings
is GestureAction.Recents -> R.string.gesture_action_recents
is GestureAction.PowerMenu -> R.string.gesture_action_power_menu
else -> R.string.gesture_action_none
})
val gestureName = stringResource(when(it.gesture) {
Gesture.DoubleTap -> R.string.preference_gesture_double_tap
Gesture.LongPress -> R.string.preference_gesture_long_press
Gesture.SwipeDown -> R.string.preference_gesture_swipe_down
Gesture.SwipeLeft -> R.string.preference_gesture_swipe_left
Gesture.SwipeRight -> R.string.preference_gesture_swipe_right
Gesture.SwipeUp -> R.string.preference_gesture_swipe_up
Gesture.HomeButton -> R.string.preference_gesture_home_button
else -> throw IllegalArgumentException("Unknown gesture: ${it.gesture}")
})
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(it)
.padding(16.dp)
.navigationBarsPadding()
) {
Text(
stringResource(R.string.gesture_failed_message, gestureName, actionName),
@@ -67,7 +73,7 @@ fun FailedGestureSheet(
text = stringResource(id = R.string.missing_permission_accessibility_gesture_failed),
secondaryAction = {
OutlinedButton(onClick = {
viewModel.disableGesture(failedGesture.gesture)
viewModel.disableGesture(it.gesture)
onDismiss()
}) {
Text(stringResource(R.string.turn_off))

View File

@@ -1,25 +1,29 @@
package de.mm20.launcher2.ui.launcher.sheets
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
@Composable
fun HiddenItemsSheet(
expanded: Boolean,
items: List<SavableSearchable>,
onDismiss: () -> Unit
) {
BottomSheetDialog(onDismiss) {
DismissableBottomSheet(expanded = expanded, onDismissRequest = onDismiss) {
SearchResultGrid(
items,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(it)
.padding(16.dp)
.navigationBarsPadding()
)
}
}

View File

@@ -5,18 +5,17 @@ import androidx.compose.runtime.Composable
@Composable
fun LauncherBottomSheets() {
val bottomSheetManager = LocalBottomSheetManager.current
bottomSheetManager.customizeSearchableSheetShown.value?.let {
CustomizeSearchableSheet(
searchable = it,
onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() })
}
if (bottomSheetManager.editFavoritesSheetShown.value) {
EditFavoritesSheet(onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
}
CustomizeSearchableSheet(
searchable = bottomSheetManager.customizeSearchableSheetShown.value,
onDismiss = { bottomSheetManager.dismissCustomizeSearchableModal() })
EditFavoritesSheet(
expanded = bottomSheetManager.editFavoritesSheetShown.value,
onDismiss = { bottomSheetManager.dismissEditFavoritesSheet() })
bottomSheetManager.editTagSheetShown.value?.let {
EditTagSheet(tag = it, onDismiss = { bottomSheetManager.dismissEditTagSheet() })
}
bottomSheetManager.failedGestureSheetShown.value?.let {
FailedGestureSheet(it, onDismiss = { bottomSheetManager.dismissFailedGestureSheet() })
}
FailedGestureSheet(
bottomSheetManager.failedGestureSheetShown.value,
onDismiss = { bottomSheetManager.dismissFailedGestureSheet() }
)
}

View File

@@ -18,20 +18,25 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -54,11 +59,11 @@ import androidx.compose.ui.unit.dp
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.ui.component.DismissableBottomSheet
import de.mm20.launcher2.widgets.AppWidget
import de.mm20.launcher2.widgets.AppWidgetConfig
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.AppsWidget
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.NotesWidget
import de.mm20.launcher2.widgets.WeatherWidget
@@ -249,41 +254,48 @@ private class BindAndConfigureAppWidgetContract(
@Composable
fun WidgetPickerSheet(
expanded: Boolean,
includeBuiltinWidgets: Boolean = true,
title: String = stringResource(R.string.widget_pick_widget),
onWidgetSelected: (Widget) -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val density = LocalDensity.current
val viewModel: WidgetPickerSheetVM = viewModel(factory = WidgetPickerSheetVM.Factory)
val bindAppWidgetStarter =
rememberLauncherForActivityResult(BindAndConfigureAppWidgetContract(density)) {
if (it != null) {
onWidgetSelected(it)
onDismiss()
}
}
val appWidgetGroups by viewModel.appWidgetGroups.collectAsState(emptyList())
val expandAllGroups by viewModel.expandAllGroups.collectAsState(false)
val colorSurface = MaterialTheme.colorScheme.surfaceContainerLow
val query by viewModel.searchQuery.collectAsState("")
BottomSheetDialog(
DismissableBottomSheet(
expanded = expanded,
onDismissRequest = onDismiss
) {
val context = LocalContext.current
val density = LocalDensity.current
val viewModel: WidgetPickerSheetVM = viewModel(factory = WidgetPickerSheetVM.Factory)
val bindAppWidgetStarter =
rememberLauncherForActivityResult(BindAndConfigureAppWidgetContract(density)) {
if (it != null) {
onWidgetSelected(it)
onDismiss()
}
}
val appWidgetGroups by viewModel.appWidgetGroups.collectAsState(emptyList())
val expandAllGroups by viewModel.expandAllGroups.collectAsState(false)
val colorSurface = MaterialTheme.colorScheme.surfaceContainerLow
val query by viewModel.searchQuery.collectAsState("")
val builtIn by viewModel.builtInWidgets.collectAsState(emptyList())
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = it
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + 16.dp
),
) {
stickyHeader {
SearchBar(
DockedSearchBar(
modifier = Modifier
.fillMaxWidth()
.drawBehind {
@@ -295,24 +307,34 @@ fun WidgetPickerSheet(
)
}
.padding(bottom = 16.dp),
windowInsets = WindowInsets(0.dp),
query = query,
onQueryChange = { viewModel.search(it) },
onSearch = {},
active = false,
onActiveChange = {},
placeholder = {
Text(stringResource(R.string.search_bar_placeholder))
},
leadingIcon = {
Icon(painterResource(R.drawable.search_24px), null)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { viewModel.search("") }) {
Icon(painterResource(R.drawable.close_24px), null)
}
}
expanded = false,
onExpandedChange = {},
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
painterResource(R.drawable.search_24px),
contentDescription = null
)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { viewModel.search("") }) {
Icon(painterResource(R.drawable.close_24px), null)
}
}
},
onSearch = {},
expanded = false,
onExpandedChange = {},
query = query,
onQueryChange = {
viewModel.search(it)
},
placeholder = {
Text(stringResource(R.string.search_bar_placeholder))
},
)
}
) {
}

View File

@@ -1,8 +1,5 @@
package de.mm20.launcher2.ui.launcher.widgets
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -11,8 +8,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
@@ -34,10 +29,6 @@ import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
@@ -161,7 +152,9 @@ fun WidgetColumn(
)
Button(
modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 8.dp),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
onClick = {
if (!editMode) {
@@ -172,7 +165,8 @@ fun WidgetColumn(
}
) {
Icon(
modifier = Modifier.padding(end = ButtonDefaults.IconSpacing)
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
painter = painterResource(
if (editMode) R.drawable.add_20px else R.drawable.edit_20px
@@ -184,13 +178,12 @@ fun WidgetColumn(
}
}
if (addNewWidget) {
WidgetPickerSheet(
onDismiss = { addNewWidget = false },
onWidgetSelected = {
viewModel.addWidget(it)
addNewWidget = false
}
)
}
WidgetPickerSheet(
expanded = addNewWidget,
onDismiss = { addNewWidget = false },
onWidgetSelected = {
viewModel.addWidget(it)
addNewWidget = false
}
)
}

View File

@@ -174,11 +174,10 @@ fun WidgetItem(
}
}
}
if (configure) {
ConfigureWidgetSheet(
widget = widget,
onWidgetUpdated = onWidgetUpdate,
onDismiss = { configure = false },
)
}
ConfigureWidgetSheet(
expanded = configure,
widget = widget,
onWidgetUpdated = onWidgetUpdate,
onDismiss = { configure = false },
)
}

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -57,7 +58,7 @@ import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.AnalogClock
@@ -144,9 +145,7 @@ fun ClockWidget(
}
}
HorizontalDivider()
if (configure) {
ConfigureClockWidgetSheet(onDismiss = { configure = false })
}
ConfigureClockWidgetSheet(expanded = configure, onDismiss = { configure = false })
}
} else {
Column(modifier = modifier) {
@@ -345,27 +344,29 @@ fun DynamicZone(
@Composable
fun ConfigureClockWidgetSheet(
expanded: Boolean,
onDismiss: () -> Unit,
) {
val viewModel: ClockWidgetSettingsScreenVM = viewModel()
val compact by viewModel.compact.collectAsState()
val color by viewModel.color.collectAsState()
val style by viewModel.clockStyle.collectAsState()
val fillHeight by viewModel.fillHeight.collectAsState()
val widgetsOnHome by viewModel.widgetsOnHome.collectAsState()
val alignment by viewModel.alignment.collectAsState()
val showSeconds by viewModel.showSeconds.collectAsState()
val monospaced by viewModel.monospaced.collectAsState()
val useAccentColor by viewModel.useThemeColor.collectAsState()
val parts by viewModel.parts.collectAsState()
val smartspacer by viewModel.useSmartspacer.collectAsState()
DismissableBottomSheet(expanded = expanded, onDismissRequest = onDismiss) {
val viewModel: ClockWidgetSettingsScreenVM = viewModel()
val compact by viewModel.compact.collectAsState()
val color by viewModel.color.collectAsState()
val style by viewModel.clockStyle.collectAsState()
val fillHeight by viewModel.fillHeight.collectAsState()
val widgetsOnHome by viewModel.widgetsOnHome.collectAsState()
val alignment by viewModel.alignment.collectAsState()
val showSeconds by viewModel.showSeconds.collectAsState()
val monospaced by viewModel.monospaced.collectAsState()
val useAccentColor by viewModel.useThemeColor.collectAsState()
val parts by viewModel.parts.collectAsState()
val smartspacer by viewModel.useSmartspacer.collectAsState()
BottomSheetDialog(onDismissRequest = onDismiss) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(it)
.padding(16.dp)
.navigationBarsPadding()
) {
Row(
modifier = Modifier.fillMaxWidth(),

View File

@@ -475,10 +475,11 @@ fun WatchFaceSelector(
}
}
if (showWidgetPicker && selected is ClockWidgetStyle.Custom) {
if (selected is ClockWidgetStyle.Custom) {
val previousWidgetId = selected.widgetId
val appWidgetHost = LocalAppWidgetHost.current
WidgetPickerSheet(
expanded = showWidgetPicker,
includeBuiltinWidgets = false,
onWidgetSelected = {
if (previousWidgetId != null) {

View File

@@ -26,8 +26,8 @@ import de.mm20.launcher2.ui.launcher.sheets.WidgetPickerSheet
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.widgets.AppWidget
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.AppsWidget
import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.MusicWidget
import de.mm20.launcher2.widgets.NotesWidget
import de.mm20.launcher2.widgets.WeatherWidget
@@ -41,7 +41,8 @@ fun AppWidget(
) {
val context = LocalContext.current
val lightBackground = if (widget.config.background) !LocalDarkTheme.current else LocalPreferDarkContentOverWallpaper.current
val lightBackground =
if (widget.config.background) !LocalDarkTheme.current else LocalPreferDarkContentOverWallpaper.current
val widgetInfo = remember(widget.config.widgetId) {
AppWidgetManager.getInstance(context)
@@ -66,28 +67,27 @@ fun AppWidget(
}
}
)
if (replaceWidget) {
WidgetPickerSheet(
onDismiss = { replaceWidget = false },
onWidgetSelected = {
val updatedWidget = when (it) {
is AppWidget -> widget.copy(
config = widget.config.copy(
widgetId = it.config.widgetId
)
WidgetPickerSheet(
expanded = replaceWidget,
onDismiss = { replaceWidget = false },
onWidgetSelected = {
val updatedWidget = when (it) {
is AppWidget -> widget.copy(
config = widget.config.copy(
widgetId = it.config.widgetId
)
)
is WeatherWidget -> it.copy(id = widget.id)
is MusicWidget -> it.copy(id = widget.id)
is CalendarWidget -> it.copy(id = widget.id)
is AppsWidget -> it.copy(id = widget.id)
is NotesWidget -> it.copy(id = widget.id)
}
onWidgetUpdate(updatedWidget)
replaceWidget = false
is WeatherWidget -> it.copy(id = widget.id)
is MusicWidget -> it.copy(id = widget.id)
is CalendarWidget -> it.copy(id = widget.id)
is AppsWidget -> it.copy(id = widget.id)
is NotesWidget -> it.copy(id = widget.id)
}
)
}
onWidgetUpdate(updatedWidget)
replaceWidget = false
}
)
} else {
val width = widget.config.width
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {

View File

@@ -36,7 +36,6 @@ 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 +56,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.Tooltip
import de.mm20.launcher2.ui.component.markdown.MarkdownEditor
import de.mm20.launcher2.ui.component.markdown.MarkdownText
@@ -404,10 +403,8 @@ fun NoteWidgetConflictResolveSheet(
onDismissRequest: () -> Unit,
) {
var selectedStrategy by remember { mutableStateOf<LinkedFileConflictStrategy?>(null) }
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
BottomSheetDialog(
DismissableBottomSheet(
onDismissRequest = onDismissRequest,
bottomSheetState = bottomSheetState
) {
Column(
@@ -546,7 +543,7 @@ fun NoteReadWriteErrorSheet(
onRelink: () -> Unit,
onUnlink: () -> Unit,
) {
BottomSheetDialog(
DismissableBottomSheet(
onDismissRequest = onDismiss,
) {
Column(

View File

@@ -115,9 +115,10 @@ fun WeatherWidget(widget: WeatherWidget) {
var showLocationDialog by remember { mutableStateOf(false) }
if (showLocationDialog) {
WeatherLocationSearchDialog(onDismissRequest = { showLocationDialog = false })
}
WeatherLocationSearchDialog(
expanded = showLocationDialog,
onDismissRequest = { showLocationDialog = false }
)
Column {

View File

@@ -14,6 +14,7 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -343,7 +344,8 @@ class SettingsActivity : BaseActivity() {
)
}
OverlayHost(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceContainer)
modifier = Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainer)
) {
NavDisplay(
backStack = backStack,

View File

@@ -1,11 +1,14 @@
package de.mm20.launcher2.ui.settings.backup
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.RestoreBackupSheet
@@ -13,24 +16,21 @@ import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@Serializable
data object BackupSettingsRoute: NavKey
data object BackupSettingsRoute : NavKey
@Composable
fun BackupSettingsScreen() {
val viewModel: BackupSettingsScreenVM = viewModel()
val restoreUri by viewModel.restoreUri
var restoreUri by remember { mutableStateOf<Uri?>(null) }
val showBackupSheet by viewModel.showBackupSheet
var showBackupSheet by remember { mutableStateOf(false) }
val restoreLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = {
viewModel.setRestoreUri(it)
restoreUri = it
}
)
@@ -41,7 +41,7 @@ fun BackupSettingsScreen() {
title = stringResource(id = R.string.preference_backup),
summary = stringResource(id = R.string.preference_backup_summary),
onClick = {
viewModel.setShowBackupSheet(true)
showBackupSheet = true
})
Preference(
title = stringResource(id = R.string.preference_restore),
@@ -55,13 +55,11 @@ fun BackupSettingsScreen() {
val uri = restoreUri
if (uri != null) {
RestoreBackupSheet(uri = uri, onDismissRequest = { viewModel.setRestoreUri(null) })
}
RestoreBackupSheet(uri = uri, onDismissRequest = { restoreUri = null })
if(showBackupSheet) {
CreateBackupSheet(onDismissRequest = {
viewModel.setShowBackupSheet(false)
CreateBackupSheet(
expanded = showBackupSheet,
onDismissRequest = {
showBackupSheet = false
})
}
}

View File

@@ -1,21 +0,0 @@
package de.mm20.launcher2.ui.settings.backup
import android.net.Uri
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.koin.core.component.KoinComponent
class BackupSettingsScreenVM : ViewModel(), KoinComponent {
val showBackupSheet = mutableStateOf(false)
val restoreUri = mutableStateOf<Uri?>(null)
fun setShowBackupSheet(show: Boolean) {
showBackupSheet.value = show
}
fun setRestoreUri(uri: Uri?) {
restoreUri.value = uri
}
}

View File

@@ -2,9 +2,11 @@ package de.mm20.launcher2.ui.settings.backup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
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.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
@@ -16,39 +18,47 @@ 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.DismissableBottomSheet
import de.mm20.launcher2.ui.component.LargeMessage
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@Composable
fun CreateBackupSheet(
expanded: Boolean,
onDismissRequest: () -> Unit
) {
DismissableBottomSheet(
expanded = expanded,
onDismissRequest = onDismissRequest
) {
val viewModel: CreateBackupSheetVM = viewModel()
val viewModel: CreateBackupSheetVM = viewModel()
val backupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/vnd.de.mm20.launcher2.backup"),
onResult = {
if (it != null) viewModel.createBackup(it)
val backupLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("application/vnd.de.mm20.launcher2.backup"),
onResult = {
if (it != null) viewModel.createBackup(it)
else onDismissRequest()
}
)
LaunchedEffect(null) {
viewModel.reset()
val fileName = "${
ZonedDateTime.now().format(
DateTimeFormatter.ISO_INSTANT
).replace(":", "_")
}.kvaesitso"
backupLauncher.launch(fileName)
}
)
LaunchedEffect(null) {
viewModel.reset()
val fileName = "${
ZonedDateTime.now().format(
DateTimeFormatter.ISO_INSTANT
).replace(":", "_")
}.kvaesitso"
backupLauncher.launch(fileName)
}
val state by viewModel.state
if (state == CreateBackupState.BackingUp || state == CreateBackupState.BackedUp) {
BottomSheetDialog(onDismissRequest) {
if (state == CreateBackupState.BackingUp) {
val state by viewModel.state
AnimatedContent(
state,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
if (it == CreateBackupState.BackingUp) {
Box(
modifier = Modifier
.fillMaxWidth()
@@ -59,8 +69,7 @@ fun CreateBackupSheet(
modifier = Modifier.size(48.dp)
)
}
}
else {
} else if (it == CreateBackupState.BackedUp) {
LargeMessage(
modifier = Modifier.aspectRatio(1f),
icon = R.drawable.check_circle_48px,

View File

@@ -34,7 +34,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.colorpicker.HctColorPicker
import de.mm20.launcher2.ui.component.colorpicker.rememberHctColorPickerState
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@@ -76,7 +76,7 @@ fun CorePaletteColorPreference(
if (showDialog) {
var currentValue by remember { mutableStateOf(value) }
BottomSheetDialog(onDismissRequest = {
DismissableBottomSheet(onDismissRequest = {
onValueChange(currentValue)
showDialog = false
}) {

View File

@@ -22,9 +22,6 @@ import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -57,7 +54,7 @@ import de.mm20.launcher2.themes.colors.StaticColor
import de.mm20.launcher2.themes.colors.atTone
import de.mm20.launcher2.themes.colors.get
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.colorpicker.HctColorPicker
import de.mm20.launcher2.ui.component.colorpicker.rememberHctColorPickerState
import de.mm20.launcher2.ui.ktx.hct
@@ -104,7 +101,7 @@ fun ThemeColorPreference(
var currentValue by remember { mutableStateOf(value) }
val actualValue = currentValue ?: defaultValue
BottomSheetDialog(onDismissRequest = {
DismissableBottomSheet(onDismissRequest = {
onValueChange(currentValue)
showDialog = false
}) {

View File

@@ -21,7 +21,7 @@ import de.mm20.launcher2.ui.launcher.sheets.EditFavoritesSheet
import kotlinx.serialization.Serializable
@Serializable
data object FavoritesSettingsRoute: NavKey
data object FavoritesSettingsRoute : NavKey
@Composable
fun FavoritesSettingsScreen() {
@@ -105,9 +105,8 @@ fun FavoritesSettingsScreen() {
}
}
if (showEditSheet) {
EditFavoritesSheet(
onDismiss = { showEditSheet = false }
)
}
EditFavoritesSheet(
expanded = showEditSheet,
onDismiss = { showEditSheet = false }
)
}

View File

@@ -14,16 +14,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
@@ -46,7 +42,7 @@ import de.mm20.launcher2.preferences.SearchBarColors
import de.mm20.launcher2.preferences.SearchBarStyle
import de.mm20.launcher2.preferences.SystemBarColors
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.SearchBar
import de.mm20.launcher2.ui.component.SearchBarLevel
import de.mm20.launcher2.ui.component.preferences.ListPreference
@@ -57,12 +53,11 @@ import de.mm20.launcher2.ui.component.preferences.SliderPreference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.widgets.clock.ConfigureClockWidgetSheet
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalBackStack
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import kotlinx.serialization.Serializable
@Serializable
data object HomescreenSettingsRoute: NavKey
data object HomescreenSettingsRoute : NavKey
@Composable
fun HomescreenSettingsScreen() {
@@ -286,9 +281,10 @@ fun HomescreenSettingsScreen() {
}
}
if (viewModel.showClockWidgetSheet) {
ConfigureClockWidgetSheet(onDismiss = { viewModel.showClockWidgetSheet = false })
}
ConfigureClockWidgetSheet(
expanded = viewModel.showClockWidgetSheet,
onDismiss = { viewModel.showClockWidgetSheet = false }
)
}
@Composable
@@ -310,7 +306,7 @@ fun SearchBarStylePreference(
val darkColors =
LocalPreferDarkContentOverWallpaper.current && colors == SearchBarColors.Auto || colors == SearchBarColors.Dark
BottomSheetDialog(
DismissableBottomSheet(
onDismissRequest = {
showDialog = false
}

View File

@@ -6,9 +6,13 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -45,8 +49,7 @@ import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.getShape
import de.mm20.launcher2.ui.component.preferences.GuardedPreference
@@ -59,7 +62,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
@Serializable
data object IconsSettingsRoute: NavKey
data object IconsSettingsRoute : NavKey
@Composable
fun IconsSettingsScreen() {
@@ -276,44 +279,49 @@ fun IconsSettingsScreen() {
}
}
if (showIconPackSheet) {
val iconPackPreviewIcons = remember(installedIconPacks, iconSize) {
installedIconPacks.associate {
it.packageName to viewModel.getIconPackPreviewIcons(
context,
it,
grid.columnCount,
iconSize,
icons?.themedIcons == true
)
}
val iconPackPreviewIcons = remember(installedIconPacks, iconSize) {
installedIconPacks.associate {
it.packageName to viewModel.getIconPackPreviewIcons(
context,
it,
grid.columnCount,
iconSize,
icons?.themedIcons == true
)
}
IconPackSelectorSheet(
installedIconPacks,
iconPackPreviewIcons = iconPackPreviewIcons,
columns = grid.columnCount,
onSelect = {
viewModel.setIconPack(it.packageName)
showIconPackSheet = false
},
onDismiss = { showIconPackSheet = false },
)
}
IconPackSelectorSheet(
expanded = showIconPackSheet,
installedIconPacks,
iconPackPreviewIcons = iconPackPreviewIcons,
columns = grid.columnCount,
onSelect = {
viewModel.setIconPack(it.packageName)
showIconPackSheet = false
},
onDismiss = { showIconPackSheet = false },
)
}
@Composable
private fun IconPackSelectorSheet(
expanded: Boolean,
installedIconPacks: List<IconPack>,
iconPackPreviewIcons: Map<String, Flow<List<LauncherIcon>>>,
columns: Int,
onSelect: (IconPack) -> Unit,
onDismiss: () -> Unit,
) {
BottomSheetDialog(onDismissRequest = onDismiss) {
DismissableBottomSheet(expanded = expanded, onDismissRequest = onDismiss) {
LazyColumn(
contentPadding = it,
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 16.dp + WindowInsets.navigationBars.asPaddingValues()
.calculateBottomPadding(),
),
) {
items(installedIconPacks.size) {

View File

@@ -19,7 +19,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.SmallMessage
import de.mm20.launcher2.ui.component.preferences.GuardedPreference
import de.mm20.launcher2.ui.component.preferences.ListPreference
@@ -399,7 +399,7 @@ fun SearchSettingsScreen() {
if (showFilterEditor) {
val filters by viewModel.searchFilters.collectAsStateWithLifecycle()
BottomSheetDialog(onDismissRequest = { showFilterEditor = false }) {
DismissableBottomSheet(onDismissRequest = { showFilterEditor = false }) {
Column(
modifier = Modifier.padding(it)
) {

View File

@@ -5,6 +5,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -16,7 +17,11 @@ 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.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -28,15 +33,19 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.DropdownMenuPopup
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.FlexibleBottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
@@ -53,7 +62,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.ToggleButton
import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -85,7 +94,7 @@ import de.mm20.launcher2.searchactions.builders.CustomIntentActionBuilder
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.BottomSheet
import de.mm20.launcher2.ui.component.ExperimentalBadge
import de.mm20.launcher2.ui.component.SearchActionIcon
import de.mm20.launcher2.ui.component.preferences.ListPreference
@@ -93,39 +102,125 @@ import de.mm20.launcher2.ui.ktx.toPixels
@Composable
fun EditSearchActionSheet(
expanded: Boolean,
initialSearchAction: CustomizableSearchActionBuilder?,
onSave: (CustomizableSearchActionBuilder) -> Unit,
onDismiss: () -> Unit,
) {
val viewModel: EditSearchActionSheetVM = viewModel()
LaunchedEffect(initialSearchAction) {
viewModel.init(initialSearchAction)
}
val page by viewModel.currentPage
val searchAction by viewModel.searchAction
BottomSheetDialog(
onDismissRequest = {
viewModel.onDismiss()
onDismiss()
}
BottomSheet(
expanded = expanded,
) {
Column(
modifier = when (page) {
EditSearchActionPage.InitAppSearch, EditSearchActionPage.PickIcon -> Modifier
else -> Modifier
.verticalScroll(rememberScrollState())
.padding(it)
var confirmDismiss by remember { mutableStateOf(false) }
val viewModel: EditSearchActionSheetVM = viewModel()
DisposableEffect(initialSearchAction) {
viewModel.init(initialSearchAction)
onDispose {
viewModel.onDismiss()
}
}
val page by viewModel.currentPage
if (page == null) return@BottomSheet
val searchAction by viewModel.searchAction
val createNew by viewModel.createNew
if (confirmDismiss) {
AlertDialog(
onDismissRequest = { confirmDismiss = false },
dismissButton = {
OutlinedButton(onClick = {
confirmDismiss = false
}) {
Text(stringResource(android.R.string.cancel))
}
},
confirmButton = {
Button(onClick = {
confirmDismiss = false
onDismiss()
}) {
Text(stringResource(R.string.action_quit))
}
},
text = {
Text(stringResource(R.string.dialog_discard_unsaved))
}
)
}
Column(
modifier = Modifier.fillMaxWidth()
) {
when (page) {
EditSearchActionPage.SelectType -> SelectTypePage(viewModel)
EditSearchActionPage.InitWebSearch -> InitWebSearchPage(viewModel)
EditSearchActionPage.InitAppSearch -> InitAppSearchPage(viewModel, it)
EditSearchActionPage.CustomizeWebSearch -> CustomizeWebSearch(viewModel)
EditSearchActionPage.CustomizeCustomIntent -> CustomizeCustomIntent(viewModel)
EditSearchActionPage.CustomizeAppSearch -> CustomizeAppSearch(viewModel)
EditSearchActionPage.PickIcon -> PickIcon(viewModel, it)
CenterAlignedTopAppBar(
title = {
if (page != EditSearchActionPage.PickIcon) {
Text(
stringResource(
if (createNew) {
R.string.create_search_action_title
} else {
R.string.edit_search_action_title
}
)
)
}
},
navigationIcon = {
if (page == EditSearchActionPage.PickIcon) {
FilledTonalIconButton(onClick = {
viewModel.applyIcon()
}) {
Icon(
painterResource(R.drawable.arrow_back_24px),
stringResource(R.string.menu_back)
)
}
}
},
actions = {
if (page != EditSearchActionPage.PickIcon) {
FilledTonalIconButton(
onClick = {
if (initialSearchAction == searchAction) {
onDismiss()
return@FilledTonalIconButton
}
when (page) {
EditSearchActionPage.CustomizeAppSearch,
EditSearchActionPage.CustomizeWebSearch,
EditSearchActionPage.CustomizeCustomIntent -> {
confirmDismiss = true
}
else -> onDismiss()
}
},
) {
Icon(
painterResource(R.drawable.close_24px),
stringResource(R.string.close)
)
}
}
},
)
AnimatedContent(
page!!,
modifier = Modifier
.fillMaxWidth()
.weight(1f, false)
) {
when (it) {
EditSearchActionPage.SelectType -> SelectTypePage(viewModel)
EditSearchActionPage.InitWebSearch -> InitWebSearchPage(viewModel)
EditSearchActionPage.InitAppSearch -> InitAppSearchPage(viewModel)
EditSearchActionPage.CustomizeWebSearch -> CustomizeWebSearch(viewModel)
EditSearchActionPage.CustomizeCustomIntent -> CustomizeCustomIntent(viewModel)
EditSearchActionPage.CustomizeAppSearch -> CustomizeAppSearch(viewModel)
EditSearchActionPage.PickIcon -> PickIcon(viewModel)
}
}
val button: (@Composable () -> Unit)? = when (page) {
@@ -170,27 +265,18 @@ fun EditSearchActionSheet(
}
}
EditSearchActionPage.PickIcon -> {
{
OutlinedButton(onClick = {
viewModel.applyIcon()
}) {
Text(stringResource(R.string.ok))
}
}
}
else -> null
}
if (button != null) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
AnimatedVisibility(button != null) {
FlexibleBottomAppBar(
horizontalArrangement = Arrangement.End,
) {
button()
Box(
modifier = Modifier.navigationBarsPadding(),
) {
button?.invoke()
}
}
}
}
@@ -201,7 +287,10 @@ fun EditSearchActionSheet(
private fun SelectTypePage(viewModel: EditSearchActionSheetVM) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(16.dp)
.navigationBarsPadding()
) {
Text(
text = stringResource(R.string.create_search_action_type),
@@ -283,13 +372,19 @@ private fun SelectTypePage(viewModel: EditSearchActionSheetVM) {
}
@Composable
private fun InitAppSearchPage(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
private fun InitAppSearchPage(viewModel: EditSearchActionSheetVM) {
val context = LocalContext.current
val searchableApps by remember { viewModel.getSearchableApps(context) }.collectAsState(null)
if (searchableApps != null) {
LazyColumn(
contentPadding = paddingValues
contentPadding = PaddingValues(
start = 16.dp,
top = 16.dp,
end = 16.dp,
bottom = 16.dp + WindowInsets.navigationBars.asPaddingValues()
.calculateBottomPadding()
)
) {
item {
Text(
@@ -335,6 +430,7 @@ private fun InitWebSearchPage(viewModel: EditSearchActionSheetVM) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = stringResource(R.string.create_search_action_website_url),
@@ -382,6 +478,8 @@ fun CustomizeWebSearch(viewModel: EditSearchActionSheetVM) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
if (searchAction != null && searchAction is CustomWebsearchActionBuilder) {
@@ -521,6 +619,8 @@ fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
if (searchAction != null) {
@@ -645,6 +745,8 @@ fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.Bottom
@@ -689,7 +791,9 @@ fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) {
Icon(
painterResource(R.drawable.check_20px),
contentDescription = null,
modifier = Modifier.padding(end = ToggleButtonDefaults.IconSpacing).size(ToggleButtonDefaults.IconSize)
modifier = Modifier
.padding(end = ToggleButtonDefaults.IconSpacing)
.size(ToggleButtonDefaults.IconSize)
)
}
Text("Data")
@@ -706,7 +810,9 @@ fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) {
Icon(
painterResource(R.drawable.check_20px),
contentDescription = null,
modifier = Modifier.padding(end = ToggleButtonDefaults.IconSpacing).size(ToggleButtonDefaults.IconSize)
modifier = Modifier
.padding(end = ToggleButtonDefaults.IconSpacing)
.size(ToggleButtonDefaults.IconSize)
)
}
Text("String extra")
@@ -859,7 +965,7 @@ fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) {
}
@Composable
fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
fun PickIcon(viewModel: EditSearchActionSheetVM) {
val action by viewModel.searchAction
val iconSizePx = 20.dp.toPixels()
@@ -869,105 +975,109 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) {
if (it != null) viewModel.importIcon(it, iconSizePx.toInt())
}
if (action?.customIcon == null) {
AnimatedContent(
action?.customIcon == null,
modifier = Modifier
.padding(16.dp)
.navigationBarsPadding()
) {
if (it) {
val availableIcons =
remember { SearchActionIcon.entries.filter { it != SearchActionIcon.Custom } }
val availableIcons =
remember { SearchActionIcon.entries.filter { it != SearchActionIcon.Custom } }
Column(
modifier = Modifier.padding(paddingValues)
) {
LazyVerticalGrid(columns = GridCells.Adaptive(64.dp)) {
if (action is AppSearchActionBuilder) {
item {
Column {
LazyVerticalGrid(columns = GridCells.Adaptive(64.dp)) {
if (action is AppSearchActionBuilder) {
item {
Box(
modifier = Modifier.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
val isSelected =
action?.icon == SearchActionIcon.Custom && action?.customIcon == null
SearchActionIconTile(isSelected, onClick = {
viewModel.setCustomIcon(null)
}) {
SearchActionIcon(
icon = SearchActionIcon.Custom,
componentName = (action as AppSearchActionBuilder).baseIntent.component,
size = 24.dp,
color = 1,
)
}
}
}
}
items(availableIcons) {
Box(
modifier = Modifier.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
val isSelected =
action?.icon == SearchActionIcon.Custom && action?.customIcon == null
val isSelected = action?.icon == it
SearchActionIconTile(isSelected, onClick = {
viewModel.setCustomIcon(null)
viewModel.setIcon(it)
}) {
SearchActionIcon(
icon = SearchActionIcon.Custom,
componentName = (action as AppSearchActionBuilder).baseIntent.component,
icon = it,
size = 24.dp,
color = 1,
color = 0,
)
}
}
}
}
items(availableIcons) {
Box(
modifier = Modifier.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
val isSelected = action?.icon == it
SearchActionIconTile(isSelected, onClick = {
viewModel.setIcon(it)
}) {
SearchActionIcon(
icon = it,
size = 24.dp,
color = 0,
)
}
}
}
}
TextButton(
modifier = Modifier.padding(vertical = 8.dp),
onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_custom_icon))
}
}
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(paddingValues)
.fillMaxWidth()
) {
SearchActionIconTile {
SearchActionIcon(builder = action!!, size = 24.dp)
}
Row(
modifier = Modifier.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = "Monochrome",
textAlign = TextAlign.End,
style = MaterialTheme.typography.labelMedium,
)
Switch(
checked = action?.iconColor == 0,
onCheckedChange = { viewModel.setIconColor(if (it) 0 else 1) })
}
Row(
modifier = Modifier
.padding(top = 24.dp)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.End
)
) {
OutlinedButton(
TextButton(
modifier = Modifier.padding(vertical = 8.dp),
onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_replace_icon))
Text(stringResource(R.string.websearch_dialog_custom_icon))
}
OutlinedButton(
onClick = { viewModel.setIcon(SearchActionIcon.Search) },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
}
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
) {
SearchActionIconTile {
SearchActionIcon(builder = action!!, size = 24.dp)
}
Row(
modifier = Modifier.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = "Monochrome",
textAlign = TextAlign.End,
style = MaterialTheme.typography.labelMedium,
)
Switch(
checked = action?.iconColor == 0,
onCheckedChange = { viewModel.setIconColor(if (it) 0 else 1) })
}
Row(
modifier = Modifier
.padding(top = 24.dp)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.End
)
) {
Text(stringResource(R.string.websearch_dialog_delete_icon))
OutlinedButton(
onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_replace_icon))
}
OutlinedButton(
onClick = { viewModel.setIcon(SearchActionIcon.Search) },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text(stringResource(R.string.websearch_dialog_delete_icon))
}
}
}
}

View File

@@ -33,7 +33,7 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent {
private var initialCustomIcon: String? = null
val currentPage = mutableStateOf(EditSearchActionPage.SelectType)
val currentPage = mutableStateOf<EditSearchActionPage?>(null)
val createNew = mutableStateOf(false)
val searchAction = mutableStateOf<CustomizableSearchActionBuilder?>(null)
@@ -207,6 +207,7 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent {
}
fun onDismiss() {
currentPage.value = null
val action = searchAction.value ?: return
val newIcon = action.customIcon
if (newIcon != initialCustomIcon) {

View File

@@ -288,26 +288,24 @@ fun SearchActionsSettingsScreen() {
val editAction by viewModel.showEditDialogFor
val createAction by viewModel.showCreateDialog
if (createAction) {
EditSearchActionSheet(
initialSearchAction = null,
onSave = {
viewModel.addAction(it)
},
onDismiss = {
viewModel.dismissDialogs()
}
)
}
if (editAction != null) {
EditSearchActionSheet(
initialSearchAction = editAction,
onSave = {
viewModel.updateAction(editAction!!, it)
},
onDismiss = {
viewModel.dismissDialogs()
}
)
}
EditSearchActionSheet(
expanded = createAction,
initialSearchAction = null,
onSave = {
viewModel.addAction(it)
},
onDismiss = {
viewModel.dismissDialogs()
}
)
EditSearchActionSheet(
expanded = editAction != null,
initialSearchAction = editAction,
onSave = {
viewModel.updateAction(editAction!!, it)
},
onDismiss = {
viewModel.dismissDialogs()
}
)
}

View File

@@ -31,10 +31,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Shapes
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -66,7 +63,7 @@ import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.serialization.UUIDSerializer
import de.mm20.launcher2.themes.shapes.CornerStyle
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.ktx.withCorners
@@ -487,7 +484,7 @@ fun ShapePreference(
val actualBottomEnd = currentBottomEnd ?: baseBottomEnd
val actualBottomStart = currentBottomStart ?: baseBottomStart
BottomSheetDialog(
DismissableBottomSheet(
onDismissRequest = {
showDialog = false
onValueChange(

View File

@@ -23,7 +23,6 @@ import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconToggleButton
@@ -33,12 +32,10 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.Typography
import androidx.compose.material3.rememberModalBottomSheetState
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
@@ -64,14 +61,13 @@ import de.mm20.launcher2.themes.typography.DefaultEmphasizedTextStyles
import de.mm20.launcher2.themes.typography.DefaultTextStyles
import de.mm20.launcher2.themes.typography.FontManager
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.theme.typography.fontFamilyOf
import de.mm20.launcher2.ui.theme.typography.typographyOf
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import java.util.UUID
import kotlin.math.ceil
@@ -763,12 +759,9 @@ private fun FontPreference(
)
if (showDialog) {
val sheetState = rememberModalBottomSheetState()
val fonts = remember { fontManager.getInstalledFonts() }
val scope = rememberCoroutineScope()
BottomSheetDialog(
DismissableBottomSheet(
onDismissRequest = { showDialog = false },
bottomSheetState = sheetState
) {
LazyColumn(contentPadding = it, verticalArrangement = Arrangement.spacedBy(12.dp)) {
if (fonts.builtIn.isNotEmpty()) {
@@ -778,11 +771,8 @@ private fun FontPreference(
null,
fonts.builtIn,
onFontClick = {
scope.launch {
sheetState.hide()
onValueChange(it)
showDialog = false
}
})
}
}
@@ -793,11 +783,8 @@ private fun FontPreference(
stringResource(R.string.font_category_device_default),
fonts.deviceDefault,
onFontClick = {
scope.launch {
sheetState.hide()
onValueChange(it)
showDialog = false
}
})
}
}
@@ -808,11 +795,8 @@ private fun FontPreference(
stringResource(R.string.font_category_generic),
fonts.generic,
onFontClick = {
scope.launch {
sheetState.hide()
onValueChange(it)
showDialog = false
}
})
}
}
@@ -984,7 +968,7 @@ private fun TextStylePreference(
mutableStateOf(value?.letterSpacing)
}
BottomSheetDialog(
DismissableBottomSheet(
onDismissRequest = {
onValueChange(
ThemeTextStyle(

View File

@@ -187,7 +187,8 @@ fun LocationPreference(
showDialog = true
}
)
if (showDialog) {
WeatherLocationSearchDialog(onDismissRequest = { showDialog = false })
}
WeatherLocationSearchDialog(
expanded = showDialog,
onDismissRequest = { showDialog = false }
)
}

View File

@@ -38,6 +38,7 @@
<string name="action_more_actions">More actions</string>
<string name="action_clear">Clear</string>
<string name="action_install">Install</string>
<string name="action_quit">Quit</string>
<!-- Delete something (a file or a web search shortcut) -->
<string name="menu_delete">Delete</string>
<string name="menu_remove">Remove</string>
@@ -751,6 +752,8 @@
<string name="search_action_contact">Add to contacts</string>
<string name="search_action_open_url">View website</string>
<string name="search_action_event">Schedule event</string>
<string name="create_search_action_title">New quick action</string>
<string name="edit_search_action_title">Edit quick action</string>
<string name="create_search_action_type">What type of action do you want to create?</string>
<string name="create_search_action_type_web">Search on a website</string>
<string name="create_search_action_type_app">Search in an app</string>
@@ -1085,4 +1088,5 @@
<string name="preference_transliteration_auto">Automatic</string>
<string name="preference_transliteration_disabled">Disabled</string>
<string name="select_tag">Select tag</string>
<string name="dialog_discard_unsaved">Quit without saving?</string>
</resources>