diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/CheckboxPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/CheckboxPreference.kt index a46909f3..54662538 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/CheckboxPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/CheckboxPreference.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.graphics.vector.ImageVector fun CheckboxPreference( title: String, icon: ImageVector? = null, - iconPadding: Boolean = true, + iconPadding: Boolean = icon != null, summary: String? = null, value: Boolean, onValueChanged: (Boolean) -> Unit, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/GuardedPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/GuardedPreference.kt new file mode 100644 index 00000000..172058d5 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/GuardedPreference.kt @@ -0,0 +1,54 @@ +package de.mm20.launcher2.ui.component.preferences + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Lock +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.Banner + +@Composable +fun GuardedPreference( + locked: Boolean, + onUnlock: (() -> Unit)? = null, + description: String, + icon: ImageVector = Icons.Rounded.Lock, + unlockLabel: String = stringResource(R.string.grant_permission), + preference: @Composable () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.extraSmall) + .background(MaterialTheme.colorScheme.surface) + ) { + if (locked) { + Banner( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 8.dp), + icon = icon, + text = description, + primaryAction = if (onUnlock != null) { + { + Button(onClick = onUnlock) { + Text(text = unlockLabel) + } + } + } else null + ) + } + preference() + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt index 9471c1af..ad0047ab 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.window.Dialog fun ListPreference( title: String, icon: ImageVector? = null, - iconPadding: Boolean = true, + iconPadding: Boolean = icon != null, items: List>, value: T, summary: String? = items.firstOrNull { value == it.value }?.label, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/Preference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/Preference.kt index bfe38f0b..541519b0 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/Preference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/Preference.kt @@ -1,15 +1,21 @@ package de.mm20.launcher2.ui.component.preferences +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp @@ -20,22 +26,28 @@ fun Preference( icon: @Composable (() -> Unit)? = null, onClick: () -> Unit = {}, controls: @Composable (() -> Unit)? = null, - enabled: Boolean = true + enabled: Boolean = true, + containerColor: Color = MaterialTheme.colorScheme.surface, ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() + .clip(MaterialTheme.shapes.extraSmall) .clickable(enabled = enabled, onClick = onClick) - .padding(horizontal = 16.dp) + .background(containerColor) + .padding( + start = if (icon != null) 8.dp else 16.dp, + end = 16.dp, + ) .alpha(if (enabled) 1f else 0.38f), ) { if (icon != null) { Box( modifier = Modifier .width(56.dp) - .padding(start = 4.dp), - contentAlignment = Alignment.CenterStart + .padding(end = 8.dp), + contentAlignment = Alignment.Center ) { icon() } @@ -45,14 +57,16 @@ fun Preference( Column( modifier = Modifier .weight(1f) - .padding(vertical = 16.dp) + .padding(vertical = 16.dp), ) { ProvideTextStyle(value = MaterialTheme.typography.titleMedium) { title() } if (summary != null) { - Spacer(modifier = Modifier.height(2.dp)) - ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodyMedium, + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant, + ) { summary() } } @@ -75,7 +89,8 @@ fun Preference( summary: String? = null, onClick: () -> Unit = {}, controls: @Composable (() -> Unit)? = null, - enabled: Boolean = true + enabled: Boolean = true, + containerColor: Color = MaterialTheme.colorScheme.surface, ) { Preference( title = { @@ -89,7 +104,8 @@ fun Preference( icon = if (iconPadding) icon else null, onClick = onClick, controls = controls, - enabled = enabled + enabled = enabled, + containerColor = containerColor, ) } @@ -98,11 +114,12 @@ fun Preference( fun Preference( title: String, icon: ImageVector? = null, - iconPadding: Boolean = true, + iconPadding: Boolean = icon != null, summary: String? = null, onClick: () -> Unit = {}, controls: @Composable (() -> Unit)? = null, - enabled: Boolean = true + enabled: Boolean = true, + containerColor: Color = MaterialTheme.colorScheme.surface, ) { Preference( title, @@ -114,6 +131,6 @@ fun Preference( tint = MaterialTheme.colorScheme.primary, ) } - }, iconPadding, summary, onClick, controls, enabled + }, iconPadding, summary, onClick, controls, enabled, containerColor, ) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceCategory.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceCategory.kt index 4c6df136..89a123ee 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceCategory.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceCategory.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @@ -15,25 +16,27 @@ fun PreferenceCategory( iconPadding: Boolean = true, content: @Composable ColumnScope.() -> Unit ) { - Column { + Column( + modifier = Modifier.fillMaxWidth(), + ) { if (title != null) { Row( modifier = Modifier - .padding(start = 16.dp, top = 16.dp, end = 16.dp) + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 8.dp) ) { Text( - modifier = Modifier.padding(start = if (iconPadding) 56.dp else 0.dp), text = title, style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.secondary ) } } - content() - Box( - modifier = Modifier.fillMaxWidth().height(0.5.dp).background( - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) - ) + Column( + modifier = Modifier + .clip(MaterialTheme.shapes.medium), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + content() + } } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt index 07978dfd..ded6500d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt @@ -1,12 +1,13 @@ package de.mm20.launcher2.ui.component.preferences -import android.net.Uri -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.compose.LocalActivity import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -16,14 +17,15 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.HelpOutline import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -31,18 +33,18 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb 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.platform.LocalContext import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.google.accompanist.systemuicontroller.rememberSystemUiController +import androidx.core.net.toUri import de.mm20.launcher2.ui.locals.LocalNavController +import de.mm20.launcher2.ui.R @Composable @@ -89,7 +91,7 @@ fun PreferenceScreen( val touchSlop = LocalViewConfiguration.current.touchSlop var fabVisible by remember { mutableStateOf(true) } val nestedScrollConnection = remember { - object: NestedScrollConnection { + object : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, @@ -102,7 +104,9 @@ fun PreferenceScreen( } } - val activity = LocalContext.current as? AppCompatActivity + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + + val activity = LocalActivity.current Scaffold( floatingActionButton = { AnimatedVisibility( @@ -116,13 +120,20 @@ fun PreferenceScreen( topBar = { CenterAlignedTopAppBar( title = title, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), navigationIcon = { IconButton(onClick = { if (navController?.navigateUp() != true) { activity?.onBackPressed() } }) { - Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "Back" + ) } }, actions = { @@ -135,25 +146,32 @@ fun PreferenceScreen( .setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb()) .build() ) - .build().launchUrl(context, Uri.parse(helpUrl)) + .build().launchUrl(context, helpUrl.toUri()) }) { Icon( imageVector = Icons.Rounded.HelpOutline, - contentDescription = "Help" + contentDescription = stringResource(R.string.help) ) } } topBarActions() - } + }, + scrollBehavior = scrollBehavior, ) - }) { + }, + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) { LazyColumn( modifier = Modifier .fillMaxSize() - .nestedScroll(nestedScrollConnection) + .nestedScroll(scrollBehavior.nestedScrollConnection) .padding(it), state = lazyColumnState, content = content, + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues( + 12.dp + ) ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceWithSwitch.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceWithSwitch.kt index 534cc9e1..f5e275f0 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceWithSwitch.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceWithSwitch.kt @@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.component.preferences import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -20,10 +21,11 @@ fun PreferenceWithSwitch( onClick: () -> Unit = {}, switchValue: Boolean, onSwitchChanged: (Boolean) -> Unit, - iconPadding: Boolean = true + iconPadding: Boolean = icon != null ) { Row( - verticalAlignment = (Alignment.CenterVertically) + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.extraSmall) ) { Box( modifier = Modifier.weight(1f) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt index 2d9de9c4..828c402e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SliderPreference.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.component.preferences +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Slider import androidx.compose.material3.Icon @@ -10,6 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import java.text.DecimalFormat import kotlin.math.floor @@ -20,6 +22,7 @@ import kotlin.math.roundToInt fun SliderPreference( title: String, icon: ImageVector? = null, + iconPadding: Boolean = icon != null, value: Float, min: Float = 0f, max: Float = 1f, @@ -33,25 +36,32 @@ fun SliderPreference( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp) + .background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.extraSmall) + .padding( + start = if (icon != null || iconPadding) 8.dp else 16.dp, + end = 16.dp, + ) .alpha(if (enabled) 1f else 0.38f), ) { - Box( - modifier = Modifier - .width(54.dp) - .padding(start = 4.dp), - contentAlignment = Alignment.CenterStart - ) { - if (icon != null) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) + if (icon != null || iconPadding) { + Box( + modifier = Modifier + .width(56.dp) + .padding(end = 8.dp), + contentAlignment = Alignment.Center + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } } } Column( modifier = Modifier.weight(1f) + .padding(vertical = 16.dp) ) { Text( modifier = Modifier.padding(start = 2.dp), @@ -71,7 +81,7 @@ fun SliderPreference( steps = step?.let { ((max - min) / it).toInt() - 1 } ?: 0, onValueChangeFinished = { onValueChanged(sliderValue) - } + }, ) if (label != null) { label(sliderValue) @@ -84,7 +94,8 @@ fun SliderPreference( Text( modifier = Modifier.width(56.dp).padding(start = 24.dp), text = format.format(sliderValue), - style = MaterialTheme.typography.titleSmall + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center, ) } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt index 4b1c3dd2..b59a7721 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/SwitchPreference.kt @@ -1,18 +1,21 @@ package de.mm20.launcher2.ui.component.preferences +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector @Composable fun SwitchPreference( title: String, icon: ImageVector? = null, - iconPadding: Boolean = true, + iconPadding: Boolean = icon != null, summary: String? = null, value: Boolean, onValueChanged: (Boolean) -> Unit, - enabled: Boolean = true + enabled: Boolean = true, + containerColor: Color = MaterialTheme.colorScheme.surface, ) { Preference( title = title, @@ -27,6 +30,7 @@ fun SwitchPreference( Switch( enabled = enabled, checked = value, onCheckedChange = onValueChanged, ) - } + }, + containerColor = containerColor, ) } \ No newline at end of file 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 e9df7668..d4b59947 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 @@ -11,6 +11,7 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider @@ -132,7 +133,9 @@ class SettingsActivity : BaseActivity() { ) ) } - OverlayHost { + OverlayHost( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceContainer) + ) { NavHost( modifier = Modifier.fillMaxSize(), navController = navController, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/buildinfo/BuildInfoSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/buildinfo/BuildInfoSettingsScreen.kt index fd5f38d9..6d8cbd29 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/buildinfo/BuildInfoSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/buildinfo/BuildInfoSettingsScreen.kt @@ -21,30 +21,32 @@ fun BuildInfoSettingsScreen() { val buildFeatures by viewModel.buildFeatures.collectAsState(emptyMap()) PreferenceScreen(title = stringResource(R.string.preference_screen_buildinfo)) { item { - Preference(title = "Build type", summary = BuildConfig.BUILD_TYPE) - var buildSignature by remember { mutableStateOf(null) } - LaunchedEffect(null) { - val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val pi = context.packageManager.getPackageInfo( - context.packageName, - PackageManager.GET_SIGNING_CERTIFICATES - ) - pi.signingInfo?.apkContentsSigners?.firstOrNull() - } else { - val pi = context.packageManager.getPackageInfo( - context.packageName, - PackageManager.GET_SIGNATURES - ) - pi.signatures?.firstOrNull() + PreferenceCategory { + Preference(title = "Build type", summary = BuildConfig.BUILD_TYPE) + var buildSignature by remember { mutableStateOf(null) } + LaunchedEffect(null) { + val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val pi = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_SIGNING_CERTIFICATES + ) + pi.signingInfo?.apkContentsSigners?.firstOrNull() + } else { + val pi = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_SIGNATURES + ) + pi.signatures?.firstOrNull() + } + val signatureHash = if (signature != null) { + val digest = MessageDigest.getInstance("SHA") + digest.update(signature.toByteArray()) + Base64.encodeToString(digest.digest(), Base64.NO_WRAP) + } else "null" + buildSignature = signatureHash } - val signatureHash = if (signature != null) { - val digest = MessageDigest.getInstance("SHA") - digest.update(signature.toByteArray()) - Base64.encodeToString(digest.digest(), Base64.NO_WRAP) - } else "null" - buildSignature = signatureHash + Preference(title = "Signature hash", summary = buildSignature) } - Preference(title = "Signature hash", summary = buildSignature) } item { PreferenceCategory(title = "Features") { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt index 143eeb2c..e7c474bd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/calendarsearch/CalendarSearchSettingsScreen.kt @@ -6,8 +6,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ErrorOutline -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -22,8 +20,8 @@ import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.ktx.sendWithBackgroundPermission import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.GuardedPreference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch @@ -47,85 +45,81 @@ fun CalendarSearchSettingsScreen() { PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) { item { PreferenceCategory { - AnimatedVisibility(hasCalendarPermission == false) { - MissingPermissionBanner( - text = stringResource(R.string.missing_permission_calendar_search_settings), - onClick = { - viewModel.requestCalendarPermission(context as AppCompatActivity) - }, - modifier = Modifier.padding(16.dp) - ) - } - PreferenceWithSwitch( - title = stringResource(R.string.preference_search_calendar), - summary = stringResource(R.string.preference_search_local_calendar_summary), - switchValue = enabledProviders.contains("local") && hasCalendarPermission == true, - onSwitchChanged = { - viewModel.setProviderEnabled("local", it) + GuardedPreference( + locked = hasCalendarPermission == false, + onUnlock = { + viewModel.requestCalendarPermission(context as AppCompatActivity) }, - enabled = hasCalendarPermission == true, - onClick = { - navController?.navigate("settings/search/calendar/local") - } - ) - if (isTasksAppInstalled) { - AnimatedVisibility(hasTasksPermission == false) { - MissingPermissionBanner( - text = stringResource(R.string.missing_permission_tasks_search_settings), - onClick = { - viewModel.requestTasksPermission(context as AppCompatActivity) - }, - modifier = Modifier.padding(16.dp) - ) - } + description = stringResource(R.string.missing_permission_calendar_search_settings), + ) { PreferenceWithSwitch( - title = stringResource(R.string.preference_search_tasks), - summary = stringResource(R.string.preference_search_tasks_summary), - switchValue = enabledProviders.contains("tasks.org") && hasTasksPermission == true, + title = stringResource(R.string.preference_search_calendar), + summary = stringResource(R.string.preference_search_local_calendar_summary), + switchValue = enabledProviders.contains("local") && hasCalendarPermission == true, onSwitchChanged = { - viewModel.setProviderEnabled("tasks.org", it) + viewModel.setProviderEnabled("local", it) }, - enabled = hasTasksPermission == true, + enabled = hasCalendarPermission == true, onClick = { - navController?.navigate("settings/search/calendar/tasks.org") + navController?.navigate("settings/search/calendar/local") } ) } - for (plugin in plugins) { - val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message - ?: stringResource(id = R.string.plugin_state_setup_required), - icon = Icons.Rounded.ErrorOutline, - primaryAction = { - TextButton(onClick = { - try { - state.setupActivity.sendWithBackgroundPermission(context) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - }) { - Text(stringResource(id = R.string.plugin_action_setup)) - } + if (isTasksAppInstalled) { + GuardedPreference( + locked = hasTasksPermission == false, + onUnlock = { + viewModel.requestTasksPermission(context as AppCompatActivity) + }, + description = stringResource(R.string.missing_permission_tasks_search_settings), + ) { + PreferenceWithSwitch( + title = stringResource(R.string.preference_search_tasks), + summary = stringResource(R.string.preference_search_tasks_summary), + switchValue = enabledProviders.contains("tasks.org") && hasTasksPermission == true, + onSwitchChanged = { + viewModel.setProviderEnabled("tasks.org", it) + }, + enabled = hasTasksPermission == true, + onClick = { + navController?.navigate("settings/search/calendar/tasks.org") } ) } - PreferenceWithSwitch( - title = plugin.plugin.label, - enabled = state is PluginState.Ready, - summary = (state as? PluginState.SetupRequired)?.message - ?: (state as? PluginState.Ready)?.text - ?: plugin.plugin.description, - switchValue = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready, - onSwitchChanged = { - viewModel.setProviderEnabled(plugin.plugin.authority, it) + } + for (plugin in plugins) { + val state = plugin.state + GuardedPreference( + locked = state is PluginState.SetupRequired, + onUnlock = { + try { + (state as PluginState.SetupRequired).setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } }, - onClick = { - navController?.navigate("settings/search/calendar/${plugin.plugin.authority}") - } - ) + description = (state as? PluginState.SetupRequired)?.message + ?: stringResource(id = R.string.plugin_state_setup_required), + icon = Icons.Rounded.ErrorOutline, + unlockLabel = stringResource(id = R.string.plugin_action_setup), + ) { + PreferenceWithSwitch( + title = plugin.plugin.label, + enabled = state is PluginState.Ready, + summary = (state as? PluginState.SetupRequired)?.message + ?: (state as? PluginState.Ready)?.text + ?: plugin.plugin.description, + switchValue = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready, + onSwitchChanged = { + viewModel.setProviderEnabled(plugin.plugin.authority, it) + }, + onClick = { + navController?.navigate("settings/search/calendar/${plugin.plugin.authority}") + } + ) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemePreferenceCategory.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemePreferenceCategory.kt index 3258550f..1cd26094 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemePreferenceCategory.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemePreferenceCategory.kt @@ -1,6 +1,8 @@ package de.mm20.launcher2.ui.settings.colorscheme +import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -10,10 +12,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.LightMode -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ColorScheme -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SegmentedButton @@ -23,6 +22,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @Composable @@ -35,12 +35,13 @@ fun ColorSchemePreferenceCategory( preview: @Composable RowScope.() -> Unit ) { Column( - modifier = Modifier.padding(top = 16.dp) + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) ) { Row( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(start = 16.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -66,21 +67,22 @@ fun ColorSchemePreferenceCategory( } } } - MaterialTheme( - colorScheme = previewColorScheme + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium), + verticalArrangement = Arrangement.spacedBy(2.dp) ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow - ) + + MaterialTheme( + colorScheme = previewColorScheme ) { Row( modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceContainerLowest, MaterialTheme.shapes.extraSmall) .horizontalScroll(rememberScrollState()) .padding(16.dp), verticalAlignment = Alignment.CenterVertically, @@ -88,10 +90,8 @@ fun ColorSchemePreferenceCategory( preview() } } + colorPreferences() } - - colorPreferences() - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt index c61d6437..53a0c4a1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemeSettingsScreen.kt @@ -66,6 +66,7 @@ import de.mm20.launcher2.themes.merge import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.ShapedLauncherIcon +import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.locals.LocalDarkTheme import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf @@ -142,17 +143,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) { if (previewDarkTheme) DefaultDarkColorScheme else DefaultLightColorScheme item { - Column( - modifier = Modifier + PreferenceCategory( + title = stringResource(R.string.preference_custom_colors_corepalette), ) { - Text( - stringResource(R.string.preference_custom_colors_corepalette), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) CorePaletteColorPreference( title = "Primary", value = theme?.corePalette?.primary, @@ -267,7 +260,6 @@ fun ColorSchemeSettingsScreen(themeId: UUID) { } }, ) - HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) } } item { @@ -681,7 +673,6 @@ fun ColorSchemeSettingsScreen(themeId: UUID) { ) }, defaultValue = selectedDefaultScheme.surfaceDim, - modifier = Modifier.padding(end = 12.dp), ) ThemeColorPreference( title = "Surface", 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 41482fd7..df18dcd7 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 @@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.settings.colorscheme import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column @@ -28,6 +29,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.stringResource @@ -56,10 +58,12 @@ fun CorePaletteColorPreference( Row( modifier = modifier.fillMaxWidth() + .clip(MaterialTheme.shapes.extraSmall) .clickable( onClick = { showDialog = true }, ) - .padding(horizontal = 16.dp, vertical = 8.dp), + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { ColorSwatch( 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 adea0f60..4bdd2f1e 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 @@ -80,10 +80,12 @@ fun ThemeColorPreference( Row( modifier = modifier.fillMaxWidth() + .clip(MaterialTheme.shapes.extraSmall) .clickable( onClick = { showDialog = true }, ) - .padding(horizontal = 16.dp, vertical = 8.dp), + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { ColorSwatch( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt index 9346d17a..52252d25 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/contacts/ContactsSettingsScreen.kt @@ -26,9 +26,9 @@ import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.GuardedPreference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen -import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch import de.mm20.launcher2.ui.component.preferences.SwitchPreference @Composable @@ -38,7 +38,10 @@ fun ContactsSettingsScreen() { val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null) val hasCallPermission by viewModel.hasCallPermission.collectAsStateWithLifecycle(null) - val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(emptyList(), minActiveState = Lifecycle.State.RESUMED) + val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle( + emptyList(), + minActiveState = Lifecycle.State.RESUMED + ) val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet()) val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(null) @@ -56,69 +59,76 @@ fun ContactsSettingsScreen() { modifier = Modifier.padding(16.dp) ) } - SwitchPreference( - title = stringResource(R.string.preference_search_contacts), - summary = stringResource(R.string.preference_search_contacts_summary), - icon = Icons.Rounded.Person, - value = enabledProviders.contains("local"), - onValueChanged = { - viewModel.setProviderEnabled("local", it) - } - ) + GuardedPreference( + locked = hasContactsPermission == false, + onUnlock = { + viewModel.requestContactsPermission(context as AppCompatActivity) + }, + description = stringResource(R.string.missing_permission_contact_search_settings), + ) { + SwitchPreference( + title = stringResource(R.string.preference_search_contacts), + summary = stringResource(R.string.preference_search_contacts_summary), + icon = Icons.Rounded.Person, + value = enabledProviders.contains("local"), + onValueChanged = { + viewModel.setProviderEnabled("local", it) + } + ) + } for (plugin in plugins) { val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message - ?: stringResource(id = R.string.plugin_state_setup_required), - icon = Icons.Rounded.ErrorOutline, - primaryAction = { - TextButton(onClick = { - try { - state.setupActivity.sendWithBackgroundPermission(context) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - }) { - Text(stringResource(id = R.string.plugin_action_setup)) - } + GuardedPreference( + locked = state is PluginState.SetupRequired, + onUnlock = { + try { + (state as PluginState.SetupRequired).setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) } + }, + description = (state as? PluginState.SetupRequired)?.message + ?: stringResource(id = R.string.plugin_state_setup_required), + icon = Icons.Rounded.ErrorOutline, + unlockLabel = stringResource(id = R.string.plugin_action_setup), + ) { + SwitchPreference( + title = plugin.plugin.label, + enabled = state is PluginState.Ready, + summary = (state as? PluginState.SetupRequired)?.message + ?: (state as? PluginState.Ready)?.text + ?: plugin.plugin.description, + value = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready, + onValueChanged = { + viewModel.setProviderEnabled(plugin.plugin.authority, it) + }, ) } - SwitchPreference( - title = plugin.plugin.label, - enabled = state is PluginState.Ready, - summary = (state as? PluginState.SetupRequired)?.message - ?: (state as? PluginState.Ready)?.text - ?: plugin.plugin.description, - value = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready, - onValueChanged = { - viewModel.setProviderEnabled(plugin.plugin.authority, it) - }, - ) } } + } + item { PreferenceCategory { - AnimatedVisibility(hasCallPermission == false) { - MissingPermissionBanner( - text = stringResource(R.string.missing_permission_call_contacts_settings), - onClick = { - viewModel.requestCallPermission(context as AppCompatActivity) + GuardedPreference( + locked = hasCallPermission == false, + onUnlock = { + viewModel.requestCallPermission(context as AppCompatActivity) + }, + description = stringResource(R.string.missing_permission_call_contacts_settings), + ) { + SwitchPreference( + title = stringResource(R.string.preference_contacts_call_on_tap), + summary = stringResource(R.string.preference_contacts_call_on_tap_summary), + icon = Icons.Rounded.Call, + value = callOnTap == true && hasCallPermission == true, + onValueChanged = { + viewModel.setCallOnTap(it) }, - modifier = Modifier.padding(16.dp) + enabled = hasCallPermission == true ) } - SwitchPreference( - title = stringResource(R.string.preference_contacts_call_on_tap), - summary = stringResource(R.string.preference_contacts_call_on_tap_summary), - icon = Icons.Rounded.Call, - value = callOnTap == true && hasCallPermission == true, - onValueChanged = { - viewModel.setCallOnTap(it) - }, - enabled = hasCallPermission == true - ) } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt index 0113df1a..e44689ff 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/filesearch/FileSearchSettingsScreen.kt @@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.settings.filesearch import android.app.PendingIntent import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -31,7 +30,7 @@ import de.mm20.launcher2.ktx.sendWithBackgroundPermission import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.Banner -import de.mm20.launcher2.ui.component.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.GuardedPreference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.SwitchPreference @@ -67,123 +66,104 @@ fun FileSearchSettingsScreen() { PreferenceCategory { val localFiles by viewModel.localFiles.collectAsState() val hasFilePermission by viewModel.hasFilePermission.collectAsState() - AnimatedVisibility(hasFilePermission == false) { - MissingPermissionBanner( - text = stringResource( - if (isAtLeastApiLevel(29)) R.string.missing_permission_file_search_settings_android10 else R.string.missing_permission_file_search_settings - ), onClick = { - viewModel.requestFilePermission(context as AppCompatActivity) + GuardedPreference( + locked = hasFilePermission == false, + onUnlock = { + viewModel.requestFilePermission(context as AppCompatActivity) + }, + description = stringResource( + if (isAtLeastApiLevel(29)) R.string.missing_permission_file_search_settings_android10 else R.string.missing_permission_file_search_settings + ), + ) { + SwitchPreference( + title = stringResource(R.string.preference_search_localfiles), + summary = stringResource(R.string.preference_search_localfiles_summary), + value = localFiles == true && hasFilePermission == true, + onValueChanged = { + viewModel.setLocalFiles(it) }, - modifier = Modifier.padding(16.dp) + enabled = hasFilePermission == true ) } - SwitchPreference( - title = stringResource(R.string.preference_search_localfiles), - summary = stringResource(R.string.preference_search_localfiles_summary), - value = localFiles == true && hasFilePermission == true, - onValueChanged = { - viewModel.setLocalFiles(it) - }, - enabled = hasFilePermission == true - ) val nextcloud by viewModel.nextcloud.collectAsState() val nextcloudAccount by viewModel.nextcloudAccount - AnimatedVisibility(nextcloudAccount == null) { - Banner( - text = stringResource(R.string.no_account_nextcloud), - icon = Icons.Rounded.AccountBox, - primaryAction = { - TextButton(onClick = { - viewModel.login( - context as AppCompatActivity, - AccountType.Nextcloud - ) - }) { - Text( - stringResource(R.string.connect_account), - ) - } + GuardedPreference( + locked = nextcloudAccount == null, + onUnlock = { + viewModel.login(context as AppCompatActivity, AccountType.Nextcloud) + }, + icon = Icons.Rounded.AccountBox, + description = stringResource(R.string.no_account_nextcloud), + unlockLabel = stringResource(R.string.connect_account), + ) { + SwitchPreference( + title = stringResource(R.string.preference_search_nextcloud), + summary = nextcloudAccount?.let { + stringResource(R.string.preference_search_cloud_summary, it.userName) + } ?: stringResource(R.string.preference_summary_not_logged_in), + value = nextcloud == true && nextcloudAccount != null, + onValueChanged = { + viewModel.setNextcloud(it) }, - modifier = Modifier.padding(16.dp) + enabled = nextcloudAccount != null ) } - SwitchPreference( - title = stringResource(R.string.preference_search_nextcloud), - summary = nextcloudAccount?.let { - stringResource(R.string.preference_search_cloud_summary, it.userName) - } ?: stringResource(R.string.preference_summary_not_logged_in), - value = nextcloud == true && nextcloudAccount != null, - onValueChanged = { - viewModel.setNextcloud(it) - }, - enabled = nextcloudAccount != null - ) val owncloud by viewModel.owncloud.collectAsState() val owncloudAccount by viewModel.owncloudAccount - AnimatedVisibility(owncloudAccount == null) { - Banner( - text = stringResource(R.string.no_account_owncloud), - icon = Icons.Rounded.AccountBox, - primaryAction = { - TextButton(onClick = { - viewModel.login( - context as AppCompatActivity, - AccountType.Owncloud - ) - }) { - Text( - stringResource(R.string.connect_account), - ) - } + GuardedPreference( + locked = owncloudAccount == null, + onUnlock = { + viewModel.login(context as AppCompatActivity, AccountType.Owncloud) + }, + icon = Icons.Rounded.AccountBox, + description = stringResource(R.string.no_account_owncloud), + unlockLabel = stringResource(R.string.connect_account), + ) { + SwitchPreference( + title = stringResource(R.string.preference_search_owncloud), + summary = owncloudAccount?.let { + stringResource(R.string.preference_search_cloud_summary, it.userName) + } ?: stringResource(R.string.preference_summary_not_logged_in), + value = owncloud == true && owncloudAccount != null, + onValueChanged = { + viewModel.setOwncloud(it) }, - modifier = Modifier.padding(16.dp) + enabled = owncloudAccount != null ) } - SwitchPreference( - title = stringResource(R.string.preference_search_owncloud), - summary = owncloudAccount?.let { - stringResource(R.string.preference_search_cloud_summary, it.userName) - } ?: stringResource(R.string.preference_summary_not_logged_in), - value = owncloud == true && owncloudAccount != null, - onValueChanged = { - viewModel.setOwncloud(it) - }, - enabled = owncloudAccount != null - ) for (plugin in plugins) { val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message ?: stringResource(id = R.string.plugin_state_setup_required), - icon = Icons.Rounded.ErrorOutline, - primaryAction = { - TextButton(onClick = { - try { - state.setupActivity.sendWithBackgroundPermission(context) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - }) { - Text(stringResource(id = R.string.plugin_action_setup)) - } + GuardedPreference( + locked = state is PluginState.SetupRequired, + onUnlock = { + try { + (state as PluginState.SetupRequired).setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) } + }, + description = (state as? PluginState.SetupRequired)?.message + ?: stringResource(id = R.string.plugin_state_setup_required), + icon = Icons.Rounded.ErrorOutline, + unlockLabel = stringResource(id = R.string.plugin_action_setup), + ) { + SwitchPreference( + title = plugin.plugin.label, + enabled = enabledPlugins != null && state is PluginState.Ready, + summary = (state as? PluginState.Ready)?.text + ?: (state as? PluginState.SetupRequired)?.message + ?: plugin.plugin.description, + value = enabledPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, + onValueChanged = { + viewModel.setPluginEnabled(plugin.plugin.authority, it) + }, ) } - SwitchPreference( - title = plugin.plugin.label, - enabled = enabledPlugins != null && state is PluginState.Ready, - summary = (state as? PluginState.Ready)?.text - ?: (state as? PluginState.SetupRequired)?.message - ?: plugin.plugin.description, - value = enabledPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, - onValueChanged = { - viewModel.setPluginEnabled(plugin.plugin.authority, it) - }, - ) } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt index 9267d9d4..2ccc8501 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/gestures/GestureSettingsScreen.kt @@ -41,6 +41,7 @@ import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.common.SearchablePicker import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.ShapedLauncherIcon +import de.mm20.launcher2.ui.component.preferences.GuardedPreference import de.mm20.launcher2.ui.component.preferences.ListPreference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen @@ -72,142 +73,137 @@ fun GestureSettingsScreen() { val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null) - AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeDown)) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_accessibility_gesture_settings), - onClick = { viewModel.requestPermission(context as AppCompatActivity) } - ) - } val swipeDownApp by viewModel.swipeDownApp.collectAsState(null) val swipeDownAppIcon by remember(swipeDownApp?.key) { viewModel.getIcon(swipeDownApp, appIconSize.toInt()) }.collectAsState(null) - GesturePreference( - title = stringResource(R.string.preference_gesture_swipe_down), - icon = Icons.Rounded.SwipeDownAlt, - value = swipeDown, - onValueChanged = { viewModel.setSwipeDown(it) }, - options = options, - app = swipeDownApp, - appIcon = swipeDownAppIcon, - onAppChanged = { viewModel.setSwipeDownApp(it) } - ) - val swipeLeft by viewModel.swipeLeft.collectAsStateWithLifecycle(null) - AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeLeft)) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_accessibility_gesture_settings), - onClick = { viewModel.requestPermission(context as AppCompatActivity) } + GuardedPreference( + locked = hasPermission == false && requiresAccessibilityService(swipeDown), + description = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onUnlock = { viewModel.requestPermission(context as AppCompatActivity) }, + ) { + GesturePreference( + title = stringResource(R.string.preference_gesture_swipe_down), + icon = Icons.Rounded.SwipeDownAlt, + value = swipeDown, + onValueChanged = { viewModel.setSwipeDown(it) }, + options = options, + app = swipeDownApp, + appIcon = swipeDownAppIcon, + onAppChanged = { viewModel.setSwipeDownApp(it) } ) } + + val swipeLeft by viewModel.swipeLeft.collectAsStateWithLifecycle(null) val swipeLeftApp by viewModel.swipeLeftApp.collectAsState(null) val swipeLeftAppIcon by remember(swipeLeftApp?.key) { viewModel.getIcon(swipeLeftApp, appIconSize.toInt()) }.collectAsState(null) - GesturePreference( - title = stringResource(R.string.preference_gesture_swipe_left), - icon = Icons.Rounded.SwipeLeftAlt, - value = swipeLeft, - onValueChanged = { viewModel.setSwipeLeft(it) }, - options = options, - app = swipeLeftApp, - appIcon = swipeLeftAppIcon, - onAppChanged = { viewModel.setSwipeLeftApp(it) } - ) - - val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null) - AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeRight)) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_accessibility_gesture_settings), - onClick = { viewModel.requestPermission(context as AppCompatActivity) } + GuardedPreference( + locked = hasPermission == false && requiresAccessibilityService(swipeLeft), + description = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onUnlock = { viewModel.requestPermission(context as AppCompatActivity) }, + ) { + GesturePreference( + title = stringResource(R.string.preference_gesture_swipe_left), + icon = Icons.Rounded.SwipeLeftAlt, + value = swipeLeft, + onValueChanged = { viewModel.setSwipeLeft(it) }, + options = options, + app = swipeLeftApp, + appIcon = swipeLeftAppIcon, + onAppChanged = { viewModel.setSwipeLeftApp(it) } ) } + + val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null) val swipeRightApp by viewModel.swipeRightApp.collectAsState(null) val swipeRightAppIcon by remember(swipeRightApp?.key) { viewModel.getIcon(swipeRightApp, appIconSize.toInt()) }.collectAsState(null) - GesturePreference( - title = stringResource(R.string.preference_gesture_swipe_right), - icon = Icons.Rounded.SwipeRightAlt, - value = swipeRight, - onValueChanged = { viewModel.setSwipeRight(it) }, - options = options, - app = swipeRightApp, - appIcon = swipeRightAppIcon, - onAppChanged = { viewModel.setSwipeRightApp(it) } - ) - - val swipeUp by viewModel.swipeUp.collectAsStateWithLifecycle(null) - AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeUp)) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_accessibility_gesture_settings), - onClick = { viewModel.requestPermission(context as AppCompatActivity) } + GuardedPreference( + locked = hasPermission == false && requiresAccessibilityService(swipeRight), + description = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onUnlock = { viewModel.requestPermission(context as AppCompatActivity) }, + ) { + GesturePreference( + title = stringResource(R.string.preference_gesture_swipe_right), + icon = Icons.Rounded.SwipeRightAlt, + value = swipeRight, + onValueChanged = { viewModel.setSwipeRight(it) }, + options = options, + app = swipeRightApp, + appIcon = swipeRightAppIcon, + onAppChanged = { viewModel.setSwipeRightApp(it) } ) } + + val swipeUp by viewModel.swipeUp.collectAsStateWithLifecycle(null) val swipeUpApp by viewModel.swipeUpApp.collectAsState(null) val swipeUpAppIcon by remember(swipeUpApp?.key) { viewModel.getIcon(swipeUpApp, appIconSize.toInt()) }.collectAsState(null) - GesturePreference( - title = stringResource(R.string.preference_gesture_swipe_up), - icon = Icons.Rounded.SwipeUpAlt, - value = swipeUp, - onValueChanged = { viewModel.setSwipeUp(it) }, - options = options, - app = swipeUpApp, - appIcon = swipeUpAppIcon, - onAppChanged = { viewModel.setSwipeUpApp(it) } - ) - - val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null) - AnimatedVisibility(hasPermission == false && requiresAccessibilityService(doubleTap)) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_accessibility_gesture_settings), - onClick = { viewModel.requestPermission(context as AppCompatActivity) } + GuardedPreference( + locked = hasPermission == false && requiresAccessibilityService(swipeUp), + description = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onUnlock = { viewModel.requestPermission(context as AppCompatActivity) }, + ) { + GesturePreference( + title = stringResource(R.string.preference_gesture_swipe_up), + icon = Icons.Rounded.SwipeUpAlt, + value = swipeUp, + onValueChanged = { viewModel.setSwipeUp(it) }, + options = options, + app = swipeUpApp, + appIcon = swipeUpAppIcon, + onAppChanged = { viewModel.setSwipeUpApp(it) } ) } + + val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null) val doubleTapApp by viewModel.doubleTapApp.collectAsState(null) val doubleTapAppIcon by remember(doubleTapApp?.key) { viewModel.getIcon(doubleTapApp, appIconSize.toInt()) }.collectAsState(null) - GesturePreference( - title = stringResource(R.string.preference_gesture_double_tap), - icon = Icons.Rounded.Adjust, - value = doubleTap, - onValueChanged = { viewModel.setDoubleTap(it) }, - options = options, - app = doubleTapApp, - appIcon = doubleTapAppIcon, - onAppChanged = { viewModel.setDoubleTapApp(it) } - ) - - val longPress by viewModel.longPress.collectAsStateWithLifecycle(null) - AnimatedVisibility(hasPermission == false && requiresAccessibilityService(longPress)) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_accessibility_gesture_settings), - onClick = { viewModel.requestPermission(context as AppCompatActivity) } + GuardedPreference( + locked = hasPermission == false && requiresAccessibilityService(doubleTap), + description = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onUnlock = { viewModel.requestPermission(context as AppCompatActivity) }, + ) { + GesturePreference( + title = stringResource(R.string.preference_gesture_double_tap), + icon = Icons.Rounded.Adjust, + value = doubleTap, + onValueChanged = { viewModel.setDoubleTap(it) }, + options = options, + app = doubleTapApp, + appIcon = doubleTapAppIcon, + onAppChanged = { viewModel.setDoubleTapApp(it) } ) } + + val longPress by viewModel.longPress.collectAsStateWithLifecycle(null) val longPressApp by viewModel.longPressApp.collectAsState(null) val longPressAppIcon by remember(longPressApp?.key) { viewModel.getIcon(longPressApp, appIconSize.toInt()) }.collectAsState(null) - GesturePreference( - title = stringResource(R.string.preference_gesture_long_press), - icon = Icons.Rounded.Circle, - value = longPress, - onValueChanged = { viewModel.setLongPress(it) }, - options = options, - app = longPressApp, - appIcon = longPressAppIcon, - onAppChanged = { viewModel.setLongPressApp(it) } - ) + GuardedPreference( + locked = hasPermission == false && requiresAccessibilityService(longPress), + description = stringResource(R.string.missing_permission_accessibility_gesture_settings), + onUnlock = { viewModel.requestPermission(context as AppCompatActivity) }, + ) { + GesturePreference( + title = stringResource(R.string.preference_gesture_long_press), + icon = Icons.Rounded.Circle, + value = longPress, + onValueChanged = { viewModel.setLongPress(it) }, + options = options, + app = longPressApp, + appIcon = longPressAppIcon, + onAppChanged = { viewModel.setLongPressApp(it) } + ) + } } } } 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 09092305..47fc79ed 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 @@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.settings.icons import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -144,24 +143,20 @@ fun IconsSettingsScreen() { item { PreferenceCategory(stringResource(R.string.preference_category_icons)) { if (previewIcons.value.isNotEmpty()) { - Surface( + Row( modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.surfaceContainer, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) + .background( + MaterialTheme.colorScheme.surfaceContainerLowest, + MaterialTheme.shapes.extraSmall + ) + .padding(vertical = 24.dp, horizontal = 8.dp) ) { - Row( - modifier = Modifier.padding(vertical = 24.dp, horizontal = 8.dp) - ) { - for (icon in previewIcons.value) { - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.Center - ) { - ShapedLauncherIcon(size = grid.iconSize.dp, icon = { icon }) - } + for (icon in previewIcons.value) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center + ) { + ShapedLauncherIcon(size = grid.iconSize.dp, icon = { icon }) } } } @@ -445,7 +440,8 @@ fun IconShapePreference( horizontalAlignment = Alignment.CenterHorizontally ) { Box( - modifier = Modifier.clip(getShape(it)) + modifier = Modifier + .clip(getShape(it)) .size(48.dp) .background(MaterialTheme.colorScheme.primary) .clickable { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/nextcloud/NextcloudSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/nextcloud/NextcloudSettingsScreen.kt index 68246087..2b6c8418 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/nextcloud/NextcloudSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/nextcloud/NextcloudSettingsScreen.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.settings.nextcloud +import androidx.activity.compose.LocalActivity import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -14,7 +15,6 @@ import androidx.compose.material.icons.automirrored.rounded.Login import androidx.compose.material.icons.automirrored.rounded.Logout import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -24,7 +24,6 @@ import androidx.compose.runtime.collectAsState 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.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -33,6 +32,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.ui.R 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.component.preferences.SwitchPreference import de.mm20.launcher2.ui.locals.LocalNavController @@ -60,10 +60,12 @@ fun NextcloudSettingsScreen() { item { Column( modifier = Modifier - .background(MaterialTheme.colorScheme.secondaryContainer) + .background( + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.shapes.medium + ) .fillParentMaxWidth() - .padding(vertical = 64.dp) - , + .padding(vertical = 64.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -72,7 +74,11 @@ fun NextcloudSettingsScreen() { modifier = Modifier .size(72.dp) .background(MaterialTheme.colorScheme.secondary, CircleShape) - .border(2.dp, MaterialTheme.colorScheme.onSecondaryContainer, CircleShape), + .border( + 2.dp, + MaterialTheme.colorScheme.onSecondaryContainer, + CircleShape + ), ) { Text( text = nextcloudUser!!.userName.split(" ") @@ -80,15 +86,17 @@ fun NextcloudSettingsScreen() { .joinToString("").let { if (it.length >= 2) it.first().toString() + it.last().toString() else it.first().toString() - } - , + }, color = MaterialTheme.colorScheme.onSecondary, style = MaterialTheme.typography.headlineMedium, ) } Text( modifier = Modifier.padding(top = 24.dp), - text = stringResource(R.string.preference_signin_user, nextcloudUser!!.userName), + text = stringResource( + R.string.preference_signin_user, + nextcloudUser!!.userName + ), color = MaterialTheme.colorScheme.onSecondaryContainer, style = MaterialTheme.typography.bodyLarge, ) @@ -104,40 +112,42 @@ fun NextcloudSettingsScreen() { modifier = Modifier .padding(end = ButtonDefaults.IconSpacing) .size(ButtonDefaults.IconSize), - contentDescription = null) + contentDescription = null + ) Text(text = stringResource(R.string.preference_signout)) } } } - item { - HorizontalDivider() - } item { - SwitchPreference( - title = stringResource(R.string.plugin_type_filesearch), - summary = stringResource(R.string.preference_search_cloud_summary, nextcloudUser!!.userName), - value = searchFiles == true, - onValueChanged = { - viewModel.setSearchFiles(it) - }, - iconPadding = false, - ) + PreferenceCategory { + SwitchPreference( + title = stringResource(R.string.plugin_type_filesearch), + summary = stringResource( + R.string.preference_search_cloud_summary, + nextcloudUser!!.userName + ), + value = searchFiles == true, + onValueChanged = { + viewModel.setSearchFiles(it) + }, + iconPadding = false, + ) + } } } else { item { - val activity = LocalContext.current as AppCompatActivity - Preference( - title = stringResource(R.string.preference_nextcloud_signin), - summary = stringResource(R.string.preference_nextcloud_signin_summary), - icon = Icons.AutoMirrored.Rounded.Login, - onClick = { - viewModel.signIn(activity) - } - ) - } - item { - HorizontalDivider() + val activity = LocalActivity.current as AppCompatActivity + PreferenceCategory { + Preference( + title = stringResource(R.string.preference_nextcloud_signin), + summary = stringResource(R.string.preference_nextcloud_signin_summary), + icon = Icons.AutoMirrored.Rounded.Login, + onClick = { + viewModel.signIn(activity) + } + ) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/owncloud/OwncloudSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/owncloud/OwncloudSettingsScreen.kt index e3308006..3e568795 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/owncloud/OwncloudSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/owncloud/OwncloudSettingsScreen.kt @@ -1,5 +1,6 @@ package de.mm20.launcher2.ui.settings.owncloud +import androidx.activity.compose.LocalActivity import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -33,6 +34,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.ui.R 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.component.preferences.SwitchPreference import de.mm20.launcher2.ui.locals.LocalNavController @@ -60,7 +62,7 @@ fun OwncloudSettingsScreen() { item { Column( modifier = Modifier - .background(MaterialTheme.colorScheme.secondaryContainer) + .background(MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.shapes.medium) .fillParentMaxWidth() .padding(vertical = 64.dp) , @@ -109,35 +111,36 @@ fun OwncloudSettingsScreen() { } } } - item { - HorizontalDivider() - } item { - SwitchPreference( - title = stringResource(R.string.plugin_type_filesearch), - summary = stringResource(R.string.preference_search_cloud_summary, owncloudUser!!.userName), - value = searchFiles == true, - onValueChanged = { - viewModel.setSearchFiles(it) - }, - iconPadding = false, - ) + PreferenceCategory { + SwitchPreference( + title = stringResource(R.string.plugin_type_filesearch), + summary = stringResource( + R.string.preference_search_cloud_summary, + owncloudUser!!.userName + ), + value = searchFiles == true, + onValueChanged = { + viewModel.setSearchFiles(it) + }, + iconPadding = false, + ) + } } } else { item { - val activity = LocalContext.current as AppCompatActivity - Preference( - title = stringResource(R.string.preference_owncloud_signin), - summary = stringResource(R.string.preference_owncloud_signin_summary), - icon = Icons.AutoMirrored.Rounded.Login, - onClick = { - viewModel.signIn(activity) - } - ) - } - item { - HorizontalDivider() + val activity = LocalActivity.current as AppCompatActivity + PreferenceCategory { + Preference( + title = stringResource(R.string.preference_owncloud_signin), + summary = stringResource(R.string.preference_owncloud_signin_summary), + icon = Icons.AutoMirrored.Rounded.Login, + onClick = { + viewModel.signIn(activity) + } + ) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt index 3303a63b..d0124caa 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/plugins/PluginSettingsScreen.kt @@ -7,7 +7,7 @@ import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -34,8 +34,8 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -59,8 +59,8 @@ import de.mm20.launcher2.ktx.sendWithBackgroundPermission import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.themes.atTone import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.preferences.CheckboxPreference +import de.mm20.launcher2.ui.component.preferences.GuardedPreference import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch @@ -142,6 +142,10 @@ fun PluginSettingsScreen(pluginId: String) { topBar = { TopAppBar( title = {}, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), navigationIcon = { IconButton(onClick = { if (navController?.navigateUp() != true) { @@ -186,7 +190,8 @@ fun PluginSettingsScreen(pluginId: String) { } } ) - } + }, + containerColor = MaterialTheme.colorScheme.surfaceContainer, ) { Column( modifier = Modifier @@ -194,10 +199,14 @@ fun PluginSettingsScreen(pluginId: String) { .padding(it) ) { Surface( - modifier = Modifier.fillMaxWidth() - .padding(bottom = 16.dp) + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + color = MaterialTheme.colorScheme.surfaceContainer, ) { - Column { + Column( + modifier = Modifier.padding(bottom = 16.dp) + ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp) @@ -253,327 +262,60 @@ fun PluginSettingsScreen(pluginId: String) { } } - val surfaceColor by animateColorAsState( - if (pluginPackage?.enabled == true) { - MaterialTheme.colorScheme.secondaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainer - } - ) - Surface( - modifier = Modifier - .fillMaxWidth(), - color = surfaceColor, + Column( + modifier = Modifier.padding(horizontal = 12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - SwitchPreference( - enabled = pluginPackage != null && hasPermission != null, - iconPadding = false, - title = stringResource(R.string.preference_plugin_enable), - value = pluginPackage?.enabled == true && hasPermission == true, - onValueChanged = { - if (hasPermission == true) { - viewModel.setPluginEnabled(it) - } else { - requestPermissionStarter.launch( - Intent().apply { - `package` = pluginPackage?.packageName - action = "de.mm20.launcher2.plugin.REQUEST_PERMISSION" - } - ) - } - } - ) - } - AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - if (filePlugins.isNotEmpty()) { - PreferenceCategory( - stringResource(R.string.plugin_type_filesearch), - iconPadding = false, - ) { - for (plugin in filePlugins) { - val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message - ?: stringResource(R.string.plugin_state_setup_required), - icon = Icons.Rounded.Info, - primaryAction = { - TextButton(onClick = { - try { - state.setupActivity.sendWithBackgroundPermission( - context - ) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - }) { - Text(stringResource(R.string.plugin_action_setup)) - } - } - ) - } else if (state is PluginState.Error) { - Banner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.plugin_state_error), - icon = Icons.Rounded.Error, - color = MaterialTheme.colorScheme.errorContainer, - ) - } - SwitchPreference( - title = plugin.plugin.label, - enabled = enabledFileSearchPlugins != null && state is PluginState.Ready, - summary = (state as? PluginState.Ready)?.text - ?: (state as? PluginState.SetupRequired)?.message - ?: plugin.plugin.description, - value = enabledFileSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, - onValueChanged = { - viewModel.setFileSearchPluginEnabled( - plugin.plugin.authority, - it - ) - }, - iconPadding = false, - ) - } - } - } - if (contactPlugins.isNotEmpty()) { - PreferenceCategory( - stringResource(R.string.plugin_type_contacts), - iconPadding = false, - ) { - for (plugin in contactPlugins) { - val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message - ?: stringResource(R.string.plugin_state_setup_required), - icon = Icons.Rounded.Info, - primaryAction = { - TextButton(onClick = { - try { - state.setupActivity.sendWithBackgroundPermission( - context - ) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - }) { - Text(stringResource(R.string.plugin_action_setup)) - } - } - ) - } else if (state is PluginState.Error) { - Banner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.plugin_state_error), - icon = Icons.Rounded.Error, - color = MaterialTheme.colorScheme.errorContainer, - ) - } - SwitchPreference( - title = plugin.plugin.label, - enabled = enabledContactPlugins != null && state is PluginState.Ready, - summary = (state as? PluginState.Ready)?.text - ?: (state as? PluginState.SetupRequired)?.message - ?: plugin.plugin.description, - value = enabledContactPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, - onValueChanged = { - viewModel.setContactPluginEnabled( - plugin.plugin.authority, - it - ) - }, - iconPadding = false, - ) - } - } - } - if (locationPlugins.isNotEmpty()) { - PreferenceCategory( - stringResource(R.string.plugin_type_locationsearch), - iconPadding = false, - ) { - for (plugin in locationPlugins) { - val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message - ?: stringResource(R.string.plugin_state_setup_required), - icon = Icons.Rounded.Info, - primaryAction = { - TextButton(onClick = { - try { - state.setupActivity.sendWithBackgroundPermission( - context - ) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - }) { - Text(stringResource(R.string.plugin_action_setup)) - } - } - ) - } else if (state is PluginState.Error) { - Banner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.plugin_state_error), - icon = Icons.Rounded.Error, - color = MaterialTheme.colorScheme.errorContainer, - ) - } - SwitchPreference( - title = plugin.plugin.label, - enabled = enabledLocationSearchPlugins != null && state is PluginState.Ready, - summary = (state as? PluginState.Ready)?.text - ?: (state as? PluginState.SetupRequired)?.message - ?: plugin.plugin.description, - value = enabledLocationSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, - onValueChanged = { - viewModel.setLocationSearchPluginEnabled( - plugin.plugin.authority, - it - ) - }, - iconPadding = false, - ) - } - } - } - if (calendarPlugins.isNotEmpty()) { - PreferenceCategory( - stringResource(R.string.plugin_type_calendar), - iconPadding = false, - ) { - val excludedCalendars by viewModel.excludedCalendars.collectAsState( - emptySet() - ) - for (plugin in calendarPlugins) { - val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message - ?: stringResource(R.string.plugin_state_setup_required), - icon = Icons.Rounded.Info, - primaryAction = { - TextButton(onClick = { - try { - state.setupActivity.sendWithBackgroundPermission( - context - ) - } catch (e: PendingIntent.CanceledException) { - CrashReporter.logException(e) - } - }) { - Text(stringResource(R.string.plugin_action_setup)) - } - } - ) - } else if (state is PluginState.Error) { - Banner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.plugin_state_error), - icon = Icons.Rounded.Error, - color = MaterialTheme.colorScheme.errorContainer, - ) - } - val calendarLists by remember(plugin.state, plugin.plugin) { - viewModel.getCalendarLists(plugin.plugin) - }.collectAsStateWithLifecycle( - null, - minActiveState = Lifecycle.State.RESUMED - ) - var showDialog by remember { mutableStateOf(false) } - val selectedCalendars = remember(excludedCalendars, calendarLists) { - calendarLists?.size?.minus(excludedCalendars.count { - it.startsWith( - plugin.plugin.authority - ) - }) - } - PreferenceWithSwitch( - title = plugin.plugin.label, - enabled = enabledCalendarSearchPlugins != null && state is PluginState.Ready, - summary = (state as? PluginState.SetupRequired)?.message - ?: if (selectedCalendars != null && calendarLists != null) { - pluralStringResource( - R.plurals.calendar_search_enabled_lists, - selectedCalendars, - selectedCalendars - ) - } - else (state as? PluginState.Ready)?.text - ?: plugin.plugin.description, - switchValue = enabledCalendarSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, - onSwitchChanged = { - viewModel.setCalendarSearchPluginEnabled( - plugin.plugin.authority, - it - ) - }, - iconPadding = false, - onClick = { - showDialog = true + PreferenceCategory { + SwitchPreference( + enabled = pluginPackage != null && hasPermission != null, + iconPadding = false, + title = stringResource(R.string.preference_plugin_enable), + value = pluginPackage?.enabled == true && hasPermission == true, + onValueChanged = { + if (hasPermission == true) { + viewModel.setPluginEnabled(it) + } else { + requestPermissionStarter.launch( + Intent().apply { + `package` = pluginPackage?.packageName + action = "de.mm20.launcher2.plugin.REQUEST_PERMISSION" } ) - if (showDialog && calendarLists?.isNotEmpty() == true) { - ModalBottomSheet( - onDismissRequest = { - showDialog = false - }, - ) { - LazyColumn { - items(calendarLists!!) { - CheckboxPreference( - title = it.name, - summary = it.owner, - iconPadding = false, - value = it.id !in excludedCalendars, - onValueChanged = { value -> - viewModel.setCalendarExcluded(it.id, !value) - }, + } + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + } + AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + if (filePlugins.isNotEmpty()) { + PreferenceCategory( + stringResource(R.string.plugin_type_filesearch), + iconPadding = false, + ) { + for (plugin in filePlugins) { + val state = plugin.state + GuardedPreference( + locked = state is PluginState.Error || state is PluginState.SetupRequired, + icon = if (state is PluginState.Error) Icons.Rounded.Error else Icons.Rounded.Info, + description = when (state) { + is PluginState.Error -> { + stringResource(R.string.plugin_state_error) + } - checkboxColors = CheckboxDefaults.colors( - checkedColor = if (it.color == 0) MaterialTheme.colorScheme.primary - else Color( - it.color.atTone(if (LocalDarkTheme.current) 80 else 40) - ), - checkmarkColor = if (it.color == 0) MaterialTheme.colorScheme.onPrimary - else Color( - it.color.atTone(if (LocalDarkTheme.current) 20 else 100) - ) - ) - ) + is PluginState.SetupRequired -> { + state.message + ?: stringResource(R.string.plugin_state_setup_required) } - } - } - } - } - } - } - if (weatherPlugins.isNotEmpty()) { - PreferenceCategory( - stringResource(R.string.plugin_type_weather), - iconPadding = false, - ) { - for (plugin in weatherPlugins) { - val state = plugin.state - if (state is PluginState.SetupRequired) { - Banner( - modifier = Modifier.padding(16.dp), - text = state.message - ?: stringResource(R.string.plugin_state_setup_required), - icon = Icons.Rounded.Info, - primaryAction = { - TextButton(onClick = { + + else -> "" + }, + onUnlock = if (state is PluginState.SetupRequired) { + { + try { state.setupActivity.sendWithBackgroundPermission( context @@ -581,40 +323,278 @@ fun PluginSettingsScreen(pluginId: String) { } catch (e: PendingIntent.CanceledException) { CrashReporter.logException(e) } - }) { - Text(stringResource(R.string.plugin_action_setup)) } + } else null + ) { + SwitchPreference( + title = plugin.plugin.label, + enabled = enabledFileSearchPlugins != null && state is PluginState.Ready, + summary = (state as? PluginState.Ready)?.text + ?: (state as? PluginState.SetupRequired)?.message + ?: plugin.plugin.description, + value = enabledFileSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, + onValueChanged = { + viewModel.setFileSearchPluginEnabled( + plugin.plugin.authority, + it + ) + }, + iconPadding = false, + ) + } + + } + } + } + if (contactPlugins.isNotEmpty()) { + PreferenceCategory( + stringResource(R.string.plugin_type_contacts), + iconPadding = false, + ) { + for (plugin in contactPlugins) { + val state = plugin.state + GuardedPreference( + locked = state is PluginState.Error || state is PluginState.SetupRequired, + icon = if (state is PluginState.Error) Icons.Rounded.Error else Icons.Rounded.Info, + description = when (state) { + is PluginState.Error -> { + stringResource(R.string.plugin_state_error) + } + + is PluginState.SetupRequired -> { + state.message + ?: stringResource(R.string.plugin_state_setup_required) + } + + else -> "" + }, + onUnlock = if (state is PluginState.SetupRequired) { + { + + try { + state.setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } + } + } else null + ) { + SwitchPreference( + title = plugin.plugin.label, + enabled = enabledContactPlugins != null && state is PluginState.Ready, + summary = (state as? PluginState.Ready)?.text + ?: (state as? PluginState.SetupRequired)?.message + ?: plugin.plugin.description, + value = enabledContactPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, + onValueChanged = { + viewModel.setContactPluginEnabled( + plugin.plugin.authority, + it + ) + }, + iconPadding = false, + ) + } + } + } + } + if (locationPlugins.isNotEmpty()) { + PreferenceCategory( + stringResource(R.string.plugin_type_locationsearch), + iconPadding = false, + ) { + for (plugin in locationPlugins) { + val state = plugin.state + GuardedPreference( + locked = state is PluginState.Error || state is PluginState.SetupRequired, + icon = if (state is PluginState.Error) Icons.Rounded.Error else Icons.Rounded.Info, + description = when (state) { + is PluginState.Error -> { + stringResource(R.string.plugin_state_error) + } + + is PluginState.SetupRequired -> { + state.message + ?: stringResource(R.string.plugin_state_setup_required) + } + + else -> "" + }, + onUnlock = if (state is PluginState.SetupRequired) { + { + + try { + state.setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } + } + } else null + ) { + SwitchPreference( + title = plugin.plugin.label, + enabled = enabledLocationSearchPlugins != null && state is PluginState.Ready, + summary = (state as? PluginState.Ready)?.text + ?: (state as? PluginState.SetupRequired)?.message + ?: plugin.plugin.description, + value = enabledLocationSearchPlugins?.contains(plugin.plugin.authority) == true && state is PluginState.Ready, + onValueChanged = { + viewModel.setLocationSearchPluginEnabled( + plugin.plugin.authority, + it + ) + }, + iconPadding = false, + ) + } + } + } + } + if (calendarPlugins.isNotEmpty()) { + PreferenceCategory( + stringResource(R.string.plugin_type_calendar), + iconPadding = false, + ) { + val excludedCalendars by viewModel.excludedCalendars.collectAsState( + emptySet() + ) + for (plugin in calendarPlugins) { + val state = plugin.state + + val calendarLists by remember(plugin.state, plugin.plugin) { + viewModel.getCalendarLists(plugin.plugin) + }.collectAsStateWithLifecycle( + null, + minActiveState = Lifecycle.State.RESUMED + ) + val selectedCalendars = + remember(excludedCalendars, calendarLists) { + calendarLists?.size?.minus(excludedCalendars.count { + it.startsWith( + plugin.plugin.authority + ) + }) } - ) - } else if (state is PluginState.Error) { - Banner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.plugin_state_error), - icon = Icons.Rounded.Error, - color = MaterialTheme.colorScheme.errorContainer, - ) + GuardedPreference( + locked = state is PluginState.Error || state is PluginState.SetupRequired, + icon = if (state is PluginState.Error) Icons.Rounded.Error else Icons.Rounded.Info, + description = when (state) { + is PluginState.Error -> { + stringResource(R.string.plugin_state_error) + } + + is PluginState.SetupRequired -> { + state.message + ?: stringResource(R.string.plugin_state_setup_required) + } + + else -> "" + }, + onUnlock = if (state is PluginState.SetupRequired) { + { + + try { + state.setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } + } + } else null + ) { + PreferenceWithSwitch( + title = plugin.plugin.label, + enabled = enabledCalendarSearchPlugins != null && state is PluginState.Ready, + summary = (state as? PluginState.SetupRequired)?.message + ?: if (selectedCalendars != null && calendarLists != null) { + pluralStringResource( + R.plurals.calendar_search_enabled_lists, + selectedCalendars, + selectedCalendars + ) + } else (state as? PluginState.Ready)?.text + ?: plugin.plugin.description, + switchValue = enabledCalendarSearchPlugins?.contains( + plugin.plugin.authority + ) == true && state is PluginState.Ready, + onSwitchChanged = { + viewModel.setCalendarSearchPluginEnabled( + plugin.plugin.authority, + it + ) + }, + iconPadding = false, + onClick = { + navController?.navigate("settings/search/calendar/${plugin.plugin.authority}") + } + ) + } + } + } + } + if (weatherPlugins.isNotEmpty()) { + PreferenceCategory( + stringResource(R.string.plugin_type_weather), + iconPadding = false, + ) { + for (plugin in weatherPlugins) { + val state = plugin.state + GuardedPreference( + locked = state is PluginState.Error || state is PluginState.SetupRequired, + icon = if (state is PluginState.Error) Icons.Rounded.Error else Icons.Rounded.Info, + description = when (state) { + is PluginState.Error -> { + stringResource(R.string.plugin_state_error) + } + + is PluginState.SetupRequired -> { + state.message + ?: stringResource(R.string.plugin_state_setup_required) + } + + else -> "" + }, + onUnlock = if (state is PluginState.SetupRequired) { + { + + try { + state.setupActivity.sendWithBackgroundPermission( + context + ) + } catch (e: PendingIntent.CanceledException) { + CrashReporter.logException(e) + } + } + } else null + ) { + Preference( + title = plugin.plugin.label, + enabled = state is PluginState.Ready && weatherProviderId != plugin.plugin.authority, + iconPadding = false, + summary = if (weatherProviderId != plugin.plugin.authority) { + stringResource(R.string.plugin_weather_provider_enable) + } else { + stringResource(R.string.plugin_weather_provider_enabled) + }, + onClick = { + viewModel.setWeatherProvider(plugin.plugin.authority) + } + ) + } } Preference( - title = plugin.plugin.label, - enabled = state is PluginState.Ready && weatherProviderId != plugin.plugin.authority, - iconPadding = false, - summary = if (weatherProviderId != plugin.plugin.authority) { - stringResource(R.string.plugin_weather_provider_enable) - } else { - stringResource(R.string.plugin_weather_provider_enabled) - }, + title = stringResource(R.string.widget_config_weather_integration_settings), + icon = Icons.AutoMirrored.Rounded.OpenInNew, onClick = { - viewModel.setWeatherProvider(plugin.plugin.authority) + navController?.navigate("settings/integrations/weather") } ) } - Preference( - title = stringResource(R.string.widget_config_weather_integration_settings), - icon = Icons.AutoMirrored.Rounded.OpenInNew, - onClick = { - navController?.navigate("settings/integrations/weather") - } - ) } } } 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 d05a87f5..5f0ff3a3 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 @@ -31,7 +31,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.icons.Wikipedia @@ -40,6 +39,7 @@ 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.SmallMessage +import de.mm20.launcher2.ui.component.preferences.GuardedPreference import de.mm20.launcher2.ui.component.preferences.ListPreference import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory @@ -65,7 +65,9 @@ fun SearchSettingsScreen() { val hasContactPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.ContactSearch } } } val isTasksAppInstalled by viewModel.isTasksAppInstalled.collectAsStateWithLifecycle() - val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(null) + val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle( + null + ) val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null) val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle(null) val hasLocationPermission by viewModel.hasLocationPermission.collectAsStateWithLifecycle(null) @@ -121,28 +123,27 @@ fun SearchSettingsScreen() { }, ) } else { - AnimatedVisibility(hasContactsPermission == false) { - MissingPermissionBanner( - text = stringResource(R.string.missing_permission_contact_search_settings), - onClick = { - viewModel.requestContactsPermission(context as AppCompatActivity) + GuardedPreference( + locked = hasContactsPermission == false, + onUnlock = { + viewModel.requestContactsPermission(context as AppCompatActivity) + }, + description = stringResource(R.string.missing_permission_contact_search_settings), + ) { + PreferenceWithSwitch( + title = stringResource(R.string.preference_search_contacts), + summary = stringResource(R.string.preference_search_contacts_summary), + icon = Icons.Rounded.Person, + switchValue = contacts == true && hasContactsPermission == true, + onSwitchChanged = { + viewModel.setContacts(it) }, - modifier = Modifier.padding(16.dp) + onClick = { + navController?.navigate("settings/search/contacts") + }, + enabled = hasContactsPermission == true ) } - PreferenceWithSwitch( - title = stringResource(R.string.preference_search_contacts), - summary = stringResource(R.string.preference_search_contacts_summary), - icon = Icons.Rounded.Person, - switchValue = contacts == true && hasContactsPermission == true, - onSwitchChanged = { - viewModel.setContacts(it) - }, - onClick = { - navController?.navigate("settings/search/contacts") - }, - enabled = hasContactsPermission == true - ) } if (hasCalendarPlugins != false || isTasksAppInstalled != false) { @@ -155,51 +156,50 @@ fun SearchSettingsScreen() { }, ) } else { - AnimatedVisibility(hasCalendarPermission == false) { - MissingPermissionBanner( - text = stringResource(R.string.missing_permission_calendar_search_settings), - onClick = { - viewModel.requestCalendarPermission(context as AppCompatActivity) + + GuardedPreference( + locked = hasCalendarPermission == false, + onUnlock = { + viewModel.requestCalendarPermission(context as AppCompatActivity) + }, + description = stringResource(R.string.missing_permission_calendar_search_settings), + ) { + PreferenceWithSwitch( + title = stringResource(R.string.preference_search_calendar), + summary = stringResource(R.string.preference_search_calendar_summary), + switchValue = calendar == true, + onSwitchChanged = { + viewModel.setCalendarSearch(it) }, - modifier = Modifier.padding(16.dp) + icon = Icons.Rounded.Today, + enabled = hasCalendarPermission == true, + onClick = { + navController?.navigate("settings/search/calendar/local") + } ) } - PreferenceWithSwitch( - title = stringResource(R.string.preference_search_calendar), - summary = stringResource(R.string.preference_search_calendar_summary), - switchValue = calendar == true, - onSwitchChanged = { - viewModel.setCalendarSearch(it) - }, - icon = Icons.Rounded.Today, - enabled = hasCalendarPermission == true, - onClick = { - navController?.navigate("settings/search/calendar/local") - } - ) } - AnimatedVisibility(hasAppShortcutsPermission == false) { - MissingPermissionBanner( - text = stringResource( - R.string.missing_permission_appshortcuts_search_settings, - stringResource(R.string.app_name) - ), - onClick = { - viewModel.requestAppShortcutsPermission(context as AppCompatActivity) - }, - modifier = Modifier.padding(16.dp) - ) - } - SwitchPreference( - title = stringResource(R.string.preference_search_appshortcuts), - summary = stringResource(R.string.preference_search_appshortcuts_summary), - icon = Icons.Rounded.AppShortcut, - value = appShortcuts == true && hasAppShortcutsPermission == true, - onValueChanged = { - viewModel.setAppShortcuts(it) + GuardedPreference( + locked = hasAppShortcutsPermission == false, + onUnlock = { + viewModel.requestAppShortcutsPermission(context as AppCompatActivity) }, - enabled = hasAppShortcutsPermission == true - ) + description = stringResource( + R.string.missing_permission_appshortcuts_search_settings, + stringResource(R.string.app_name), + ), + ) { + SwitchPreference( + title = stringResource(R.string.preference_search_appshortcuts), + summary = stringResource(R.string.preference_search_appshortcuts_summary), + icon = Icons.Rounded.AppShortcut, + value = appShortcuts == true && hasAppShortcutsPermission == true, + onValueChanged = { + viewModel.setAppShortcuts(it) + }, + enabled = hasAppShortcutsPermission == true + ) + } SwitchPreference( title = stringResource(R.string.preference_search_calculator), @@ -246,43 +246,38 @@ fun SearchSettingsScreen() { viewModel.setWebsites(it) } ) - - AnimatedVisibility(hasLocationPermission == false) { - MissingPermissionBanner( - text = stringResource( - R.string.missing_permission_location_search, - ), - onClick = { - viewModel.requestLocationPermission(context as AppCompatActivity) - }, - modifier = Modifier.padding(16.dp) - ) - } - - if (hasLocationPlugins != false) { - Preference( - title = stringResource(R.string.preference_search_locations), - summary = stringResource(R.string.preference_search_locations_summary), - icon = Icons.Rounded.Place, - enabled = hasLocationPermission == true, - onClick = { - navController?.navigate("settings/search/locations") - } - ) - } else { - PreferenceWithSwitch( - title = stringResource(R.string.preference_search_locations), - summary = stringResource(R.string.preference_search_locations_summary), - icon = Icons.Rounded.Place, - onClick = { - navController?.navigate("settings/search/locations") - }, - switchValue = places == true, - onSwitchChanged = { - viewModel.setPlacesSearch(it) - }, - enabled = hasLocationPermission == true, - ) + GuardedPreference( + locked = hasLocationPermission == false, + onUnlock = { + viewModel.requestLocationPermission(context as AppCompatActivity) + }, + description = stringResource(R.string.missing_permission_location_search), + ) { + if (hasLocationPlugins != false) { + Preference( + title = stringResource(R.string.preference_search_locations), + summary = stringResource(R.string.preference_search_locations_summary), + icon = Icons.Rounded.Place, + enabled = hasLocationPermission == true, + onClick = { + navController?.navigate("settings/search/locations") + } + ) + } else { + PreferenceWithSwitch( + title = stringResource(R.string.preference_search_locations), + summary = stringResource(R.string.preference_search_locations_summary), + icon = Icons.Rounded.Place, + onClick = { + navController?.navigate("settings/search/locations") + }, + switchValue = places == true, + onSwitchChanged = { + viewModel.setPlacesSearch(it) + }, + enabled = hasLocationPermission == true, + ) + } } Preference( @@ -327,6 +322,7 @@ fun SearchSettingsScreen() { ) SwitchPreference( title = stringResource(R.string.preference_filter_bar), + iconPadding = true, summary = stringResource(R.string.preference_filter_bar_summary), value = filterBar == true, onValueChanged = { @@ -336,6 +332,7 @@ fun SearchSettingsScreen() { AnimatedVisibility(filterBar == true) { Preference( title = stringResource(R.string.preference_customize_filter_bar), + iconPadding = true, summary = stringResource(R.string.preference_customize_filter_bar_summary), onClick = { navController?.navigate("settings/search/filterbar") @@ -357,6 +354,7 @@ fun SearchSettingsScreen() { ) SwitchPreference( title = stringResource(R.string.preference_search_bar_launch_on_enter), + iconPadding = true, summary = stringResource(R.string.preference_search_bar_launch_on_enter_summary), value = launchOnEnter == true, onValueChanged = { 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 4a2c1399..ef24516b 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 @@ -29,8 +29,6 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FilterChip import androidx.compose.material3.FloatingActionButton @@ -53,6 +51,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -222,6 +221,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) { } ) } + } + item { PreferenceCategory { ShapePreview( previewShapes = previewShapes, @@ -246,6 +247,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) { } ) } + } + item { PreferenceCategory { ShapePreview( previewShapes = previewShapes, @@ -279,7 +282,9 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) { } ) } - PreferenceCategory { + } + item { + PreferenceCategory(title = "Large") { ShapePreview( previewShapes = previewShapes, ) { @@ -299,6 +304,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) { } ) } + } + item { PreferenceCategory { ShapePreference( title = "Large increased", @@ -312,6 +319,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) { } ) } + } + item { PreferenceCategory { ShapePreview( previewShapes = previewShapes, @@ -344,6 +353,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) { } ) } + } + item { PreferenceCategory { ShapePreference( title = "Extra large increased", @@ -357,6 +368,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) { } ) } + } + item { PreferenceCategory { ShapePreference( title = "Extra extra large", @@ -398,11 +411,12 @@ fun ShapePreference( Row( modifier = Modifier .fillMaxWidth() - .padding(bottom = 8.dp) + .clip(MaterialTheme.shapes.extraSmall) + .background(MaterialTheme.colorScheme.surface) .clickable( onClick = { showDialog = true }, ) - .padding(horizontal = 16.dp, vertical = 8.dp), + .padding(16.dp), verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -706,28 +720,20 @@ private fun ShapePreview( previewShapes: Shapes, content: @Composable () -> Unit, ) { - MaterialTheme( - shapes = previewShapes + Row( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.extraSmall) + .background(MaterialTheme.colorScheme.surfaceContainerLowest) + .horizontalScroll(rememberScrollState()) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer - ) + MaterialTheme( + shapes = previewShapes ) { - - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - content() - } + content() } } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreen.kt index 24293308..5eee3594 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tasks/TasksSettingsScreen.kt @@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.settings.tasks import androidx.activity.compose.LocalActivity import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.OpenInNew @@ -9,6 +10,7 @@ import androidx.compose.material.icons.rounded.CheckCircle import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.TaskAlt import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -19,7 +21,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.MissingPermissionBanner +import de.mm20.launcher2.ui.component.preferences.GuardedPreference import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceScreen @@ -41,55 +43,63 @@ fun TasksIntegrationSettingsScreen() { ) { if (isTasksInstalled == false) { item { - Banner( - text = stringResource( - R.string.preference_tasks_integration_description, - stringResource(R.string.app_name) - ), - icon = Icons.Rounded.Info, - modifier = Modifier.padding(16.dp), - primaryAction = { - Button(onClick = { - viewModel.downloadTasksApp(activity as AppCompatActivity) - }) { - Text(stringResource(R.string.action_install)) + PreferenceCategory { + Banner( + text = stringResource( + R.string.preference_tasks_integration_description, + stringResource(R.string.app_name) + ), + icon = Icons.Rounded.Info, + modifier = Modifier + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp), + primaryAction = { + Button(onClick = { + viewModel.downloadTasksApp(activity as AppCompatActivity) + }) { + Text(stringResource(R.string.action_install)) + } } - } - ) + ) + } } } if (isTasksInstalled == true) { item { PreferenceCategory { - if (hasTasksPermission == false) { - MissingPermissionBanner( - modifier = Modifier.padding(16.dp), - text = stringResource(R.string.missing_permission_tasks_integration), - onClick = { - viewModel.requestTasksPermission(activity as AppCompatActivity) - } - ) - } if (hasTasksPermission == true) { Banner( text = stringResource(R.string.preference_tasks_integration_ready), icon = Icons.Rounded.CheckCircle, - modifier = Modifier.padding(16.dp), + modifier = Modifier + .background( + MaterialTheme.colorScheme.surface, + MaterialTheme.shapes.extraSmall + ) + .padding(16.dp), ) } - PreferenceWithSwitch( - icon = Icons.Rounded.TaskAlt, - title = stringResource(R.string.preference_search_tasks), - summary = stringResource(R.string.preference_search_tasks_summary), - switchValue = isTasksSearchEnabled == true && hasTasksPermission == true, - onSwitchChanged = { - viewModel.setTasksSearchEnabled(it) + GuardedPreference( + locked = hasTasksPermission == false, + onUnlock = { + viewModel.requestTasksPermission(activity as AppCompatActivity) }, - enabled = hasTasksPermission == true, - onClick = { - navController?.navigate("settings/search/calendar/tasks.org") - } - ) + description = stringResource(R.string.missing_permission_tasks_integration), + ) { + PreferenceWithSwitch( + icon = Icons.Rounded.TaskAlt, + title = stringResource(R.string.preference_search_tasks), + summary = stringResource(R.string.preference_search_tasks_summary), + switchValue = isTasksSearchEnabled == true && hasTasksPermission == true, + onSwitchChanged = { + viewModel.setTasksSearchEnabled(it) + }, + enabled = hasTasksPermission == true, + onClick = { + navController?.navigate("settings/search/calendar/tasks.org") + } + ) + } Preference( title = stringResource(R.string.preference_launch_tasks_app), icon = Icons.AutoMirrored.Rounded.OpenInNew, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/unitconverter/UnitConverterSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/unitconverter/UnitConverterSettingsScreen.kt index 95283fda..720a6087 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/unitconverter/UnitConverterSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/unitconverter/UnitConverterSettingsScreen.kt @@ -45,6 +45,8 @@ fun UnitConverterSettingsScreen() { } ) } + } + item { PreferenceCategory { Preference( title = stringResource(R.string.preference_search_supportedunits), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt index 85b2687f..39a51292 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt @@ -10,19 +10,34 @@ fun makeTypography( val baseTypography = Typography() return Typography( displayLarge = baseTypography.displayLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium), + displayLargeEmphasized = baseTypography.displayLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), displayMedium = baseTypography.displayMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium), + displayMediumEmphasized = baseTypography.displayMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), displaySmall = baseTypography.displaySmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium), + displaySmallEmphasized = baseTypography.displaySmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), headlineLarge = baseTypography.headlineLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), + headlineLargeEmphasized = baseTypography.headlineLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold), headlineMedium = baseTypography.headlineMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), + headlineMediumEmphasized = baseTypography.headlineMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold), headlineSmall = baseTypography.headlineSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), + headlineSmallEmphasized = baseTypography.headlineSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold), titleLarge = baseTypography.titleLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), - titleMedium = baseTypography.titleMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), + titleLargeEmphasized = baseTypography.titleLargeEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold), + titleMedium = baseTypography.titleMedium.copy(fontFamily = headlineFamily), + titleMediumEmphasized = baseTypography.titleMediumEmphasized.copy(fontFamily = headlineFamily), titleSmall = baseTypography.titleSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), + titleSmallEmphasized = baseTypography.titleSmallEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold), bodyLarge = baseTypography.bodyLarge.copy(fontFamily = bodyFamily), + bodyLargeEmphasized = baseTypography.bodyLargeEmphasized.copy(fontFamily = bodyFamily, fontWeight = FontWeight.Medium), bodyMedium = baseTypography.bodyMedium.copy(fontFamily = bodyFamily), + bodyMediumEmphasized = baseTypography.bodyMediumEmphasized.copy(fontFamily = bodyFamily, fontWeight = FontWeight.Medium), bodySmall = baseTypography.bodySmall.copy(fontFamily = bodyFamily), - labelLarge = baseTypography.labelLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), - labelMedium = baseTypography.labelMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), - labelSmall = baseTypography.labelSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold) + bodySmallEmphasized = baseTypography.bodySmallEmphasized.copy(fontFamily = bodyFamily, fontWeight = FontWeight.Medium), + labelLarge = baseTypography.labelLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium), + labelLargeEmphasized = baseTypography.labelLargeEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), + labelMedium = baseTypography.labelMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium), + labelMediumEmphasized = baseTypography.labelMediumEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), + labelSmall = baseTypography.labelSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium), + labelSmallEmphasized = baseTypography.labelSmallEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), ) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/Outfit.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/Outfit.kt index 34c70d1b..fe2eb32f 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/Outfit.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/Outfit.kt @@ -11,6 +11,7 @@ val Outfit = FontFamily( Font(R.font.outfit_200, weight = FontWeight.ExtraLight, style = FontStyle.Normal), Font(R.font.outfit_300, weight = FontWeight.Light, style = FontStyle.Normal), Font(R.font.outfit_400, weight = FontWeight.Normal, style = FontStyle.Normal), + Font(R.font.outfit_500, weight = FontWeight.Medium, style = FontStyle.Normal), Font(R.font.outfit_600, weight = FontWeight.SemiBold, style = FontStyle.Normal), Font(R.font.outfit_700, weight = FontWeight.Bold, style = FontStyle.Normal), Font(R.font.outfit_800, weight = FontWeight.ExtraBold, style = FontStyle.Normal),