From 0c2782b35a8cc852935e9d107a6c58f2307272d4 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:09:29 +0100 Subject: [PATCH] [WIP] reimplement bottom sheets Fix #1725 --- .../launcher2/ui/common/RestoreBackupSheet.kt | 217 ++++++----- .../launcher2/ui/common/SearchablePicker.kt | 4 +- .../ui/common/WeatherLocationSearchDialog.kt | 47 ++- .../common/WeatherLocationSearchDialogVM.kt | 4 +- .../launcher2/ui/component/BottomSheet.kt | 159 ++++++++ .../ui/component/BottomSheetDialog.kt | 39 -- .../ui/component/DismissableBottomSheet.kt | 346 ++++++++++++++++ .../ui/launcher/search/SearchColumn.kt | 9 +- .../launcher/sheets/ConfigureWidgetSheet.kt | 56 +-- .../sheets/CustomizeSearchableSheet.kt | 41 +- .../ui/launcher/sheets/EditFavoritesSheet.kt | 40 +- .../ui/launcher/sheets/EditTagSheet.kt | 9 +- .../ui/launcher/sheets/FailedGestureSheet.kt | 60 +-- .../ui/launcher/sheets/HiddenItemsSheet.kt | 10 +- .../launcher/sheets/LauncherBottomSheets.kt | 21 +- .../ui/launcher/sheets/WidgetPickerSheet.kt | 110 +++--- .../ui/launcher/widgets/WidgetColumn.kt | 33 +- .../ui/launcher/widgets/WidgetItem.kt | 13 +- .../ui/launcher/widgets/clock/ClockWidget.kt | 37 +- .../widgets/clock/WatchFaceSelector.kt | 3 +- .../ui/launcher/widgets/external/AppWidget.kt | 42 +- .../ui/launcher/widgets/notes/NotesWidget.kt | 9 +- .../launcher/widgets/weather/WeatherWidget.kt | 7 +- .../launcher2/ui/settings/SettingsActivity.kt | 4 +- .../settings/backup/BackupSettingsScreen.kt | 34 +- .../settings/backup/BackupSettingsScreenVM.kt | 21 - .../ui/settings/backup/CreateBackupSheet.kt | 57 +-- .../colorscheme/CorePaletteColorPreference.kt | 4 +- .../colorscheme/ThemeColorPreference.kt | 7 +- .../favorites/FavoritesSettingsScreen.kt | 11 +- .../homescreen/HomescreenSettingsScreen.kt | 18 +- .../ui/settings/icons/IconsSettingsScreen.kt | 64 +-- .../settings/search/SearchSettingsScreen.kt | 4 +- .../searchactions/EditSearchActionSheet.kt | 368 ++++++++++++------ .../searchactions/EditSearchActionSheetVM.kt | 3 +- .../SearchActionsSettingsScreen.kt | 42 +- .../shapes/ShapeSchemeSettingsScreen.kt | 7 +- .../typography/TypographySettingsScreen.kt | 22 +- .../WeatherIntegrationSettingsScreen.kt | 7 +- core/i18n/src/main/res/values/strings.xml | 4 + 40 files changed, 1315 insertions(+), 678 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheet.kt delete mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/component/DismissableBottomSheet.kt delete mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreenVM.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt index fb19177a4..4ff90716f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt @@ -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)) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt index fb160ba56..3e626993c 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/SearchablePicker.kt @@ -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() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt index f47d05a06..23696693b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialog.kt @@ -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) ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt index 10d44dfa5..f4324608b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/WeatherLocationSearchDialogVM.kt @@ -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 } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheet.kt new file mode 100644 index 000000000..e9e9c9970 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheet.kt @@ -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 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 + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt deleted file mode 100644 index c0e8ed2a4..000000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt +++ /dev/null @@ -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, - )) - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/DismissableBottomSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/DismissableBottomSheet.kt new file mode 100644 index 000000000..34b5acf94 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/DismissableBottomSheet.kt @@ -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 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() + + 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, + 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.newOffsetForDelta(delta: Float) = + ((if (offset.isNaN()) 0f else offset) + delta).coerceIn( + anchoredDraggableState.anchors.minPosition(), + anchoredDraggableState.anchors.maxPosition(), + ) + + private suspend fun AnchoredDraggableState.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 + } + } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt index 5f94ff900..f92be26fa 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchColumn.kt @@ -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() }) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt index 51dc54ab8..c7df8bb94 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/ConfigureWidgetSheet.kt @@ -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 } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt index a614000c9..d66b7c379 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt @@ -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, ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt index 3859103c1..164722594 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt @@ -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) } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheet.kt index 07cc134b1..c5a99234d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheet.kt @@ -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() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt index 7171a7920..61d3d2c49 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/FailedGestureSheet.kt @@ -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)) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/HiddenItemsSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/HiddenItemsSheet.kt index 219bddc2f..91dd4d8c3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/HiddenItemsSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/HiddenItemsSheet.kt @@ -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, onDismiss: () -> Unit ) { - BottomSheetDialog(onDismiss) { + DismissableBottomSheet(expanded = expanded, onDismissRequest = onDismiss) { SearchResultGrid( items, modifier = Modifier .verticalScroll(rememberScrollState()) - .padding(it) + .padding(16.dp) + .navigationBarsPadding() ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt index d2ef8ac86..0c581db52 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt @@ -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() } + ) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt index fcbbf6b3e..26aac63bc 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/WidgetPickerSheet.kt @@ -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)) + }, + ) } ) { } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt index a17a406ba..100fb00be 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetColumn.kt @@ -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 + } + ) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt index dd15c5337..cb19f287b 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/WidgetItem.kt @@ -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 }, + ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt index ff6244719..cabc10d81 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidget.kt @@ -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(), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/WatchFaceSelector.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/WatchFaceSelector.kt index f9d41728c..64816fb37 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/WatchFaceSelector.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/WatchFaceSelector.kt @@ -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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/AppWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/AppWidget.kt index 779347213..85d1e6340 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/AppWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/external/AppWidget.kt @@ -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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt index 1a26120aa..52662bf39 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt @@ -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(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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt index fc45f58e7..bec7df144 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt @@ -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 { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index ad8295558..1dd4e4876 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreen.kt index cdad17bde..525f53b3e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreen.kt @@ -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(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 }) - } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreenVM.kt deleted file mode 100644 index 3a7df897c..000000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/BackupSettingsScreenVM.kt +++ /dev/null @@ -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(null) - - fun setShowBackupSheet(show: Boolean) { - showBackupSheet.value = show - } - - fun setRestoreUri(uri: Uri?) { - restoreUri.value = uri - } -} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt index 69f7a0b1b..ae65a7fcd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt @@ -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, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt index bd0659fc2..89c554cda 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt @@ -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 }) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt index acb59ee51..501ede1f8 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt @@ -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 }) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt index 379253d5c..c940b5e07 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/favorites/FavoritesSettingsScreen.kt @@ -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 } + ) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt index d9caee156..14eb97ccf 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/homescreen/HomescreenSettingsScreen.kt @@ -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 } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt index 6ccef031a..21b65b176 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/icons/IconsSettingsScreen.kt @@ -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, iconPackPreviewIcons: Map>>, 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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt index 8d4ae75f2..1f871dd21 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/search/SearchSettingsScreen.kt @@ -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) ) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt index c9e3f2145..34ef0ca76 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt @@ -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)) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt index 8e8096b3b..a62f093a7 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt @@ -33,7 +33,7 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent { private var initialCustomIcon: String? = null - val currentPage = mutableStateOf(EditSearchActionPage.SelectType) + val currentPage = mutableStateOf(null) val createNew = mutableStateOf(false) val searchAction = mutableStateOf(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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt index b909275f3..7581a3ece 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt @@ -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() + } + ) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemeSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemeSettingsScreen.kt index df219c316..6ed1a2b56 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemeSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemeSettingsScreen.kt @@ -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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt index 811fdbf71..51306af5e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt @@ -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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt index 028001b93..ecc1528f9 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt @@ -187,7 +187,8 @@ fun LocationPreference( showDialog = true } ) - if (showDialog) { - WeatherLocationSearchDialog(onDismissRequest = { showDialog = false }) - } + WeatherLocationSearchDialog( + expanded = showDialog, + onDismissRequest = { showDialog = false } + ) } \ No newline at end of file diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index fbb2baf6e..4eb1e009b 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ More actions Clear Install + Quit Delete Remove @@ -751,6 +752,8 @@ Add to contacts View website Schedule event + New quick action + Edit quick action What type of action do you want to create? Search on a website Search in an app @@ -1085,4 +1088,5 @@ Automatic Disabled Select tag + Quit without saving?