Settings facelift

This commit is contained in:
MM20 2025-06-03 00:12:07 +02:00
parent fcc111b048
commit b6d533466b
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
29 changed files with 1138 additions and 1026 deletions

View File

@ -10,7 +10,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
fun CheckboxPreference( fun CheckboxPreference(
title: String, title: String,
icon: ImageVector? = null, icon: ImageVector? = null,
iconPadding: Boolean = true, iconPadding: Boolean = icon != null,
summary: String? = null, summary: String? = null,
value: Boolean, value: Boolean,
onValueChanged: (Boolean) -> Unit, onValueChanged: (Boolean) -> Unit,

View File

@ -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()
}
}

View File

@ -17,7 +17,7 @@ import androidx.compose.ui.window.Dialog
fun <T> ListPreference( fun <T> ListPreference(
title: String, title: String,
icon: ImageVector? = null, icon: ImageVector? = null,
iconPadding: Boolean = true, iconPadding: Boolean = icon != null,
items: List<ListPreferenceItem<T>>, items: List<ListPreferenceItem<T>>,
value: T, value: T,
summary: String? = items.firstOrNull { value == it.value }?.label, summary: String? = items.firstOrNull { value == it.value }?.label,

View File

@ -1,15 +1,21 @@
package de.mm20.launcher2.ui.component.preferences package de.mm20.launcher2.ui.component.preferences
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha 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.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -20,22 +26,28 @@ fun Preference(
icon: @Composable (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
controls: @Composable (() -> Unit)? = null, controls: @Composable (() -> Unit)? = null,
enabled: Boolean = true enabled: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(MaterialTheme.shapes.extraSmall)
.clickable(enabled = enabled, onClick = onClick) .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), .alpha(if (enabled) 1f else 0.38f),
) { ) {
if (icon != null) { if (icon != null) {
Box( Box(
modifier = Modifier modifier = Modifier
.width(56.dp) .width(56.dp)
.padding(start = 4.dp), .padding(end = 8.dp),
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.Center
) { ) {
icon() icon()
} }
@ -45,14 +57,16 @@ fun Preference(
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(vertical = 16.dp) .padding(vertical = 16.dp),
) { ) {
ProvideTextStyle(value = MaterialTheme.typography.titleMedium) { ProvideTextStyle(value = MaterialTheme.typography.titleMedium) {
title() title()
} }
if (summary != null) { if (summary != null) {
Spacer(modifier = Modifier.height(2.dp)) CompositionLocalProvider(
ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { LocalTextStyle provides MaterialTheme.typography.bodyMedium,
LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
) {
summary() summary()
} }
} }
@ -75,7 +89,8 @@ fun Preference(
summary: String? = null, summary: String? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
controls: @Composable (() -> Unit)? = null, controls: @Composable (() -> Unit)? = null,
enabled: Boolean = true enabled: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface,
) { ) {
Preference( Preference(
title = { title = {
@ -89,7 +104,8 @@ fun Preference(
icon = if (iconPadding) icon else null, icon = if (iconPadding) icon else null,
onClick = onClick, onClick = onClick,
controls = controls, controls = controls,
enabled = enabled enabled = enabled,
containerColor = containerColor,
) )
} }
@ -98,11 +114,12 @@ fun Preference(
fun Preference( fun Preference(
title: String, title: String,
icon: ImageVector? = null, icon: ImageVector? = null,
iconPadding: Boolean = true, iconPadding: Boolean = icon != null,
summary: String? = null, summary: String? = null,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
controls: @Composable (() -> Unit)? = null, controls: @Composable (() -> Unit)? = null,
enabled: Boolean = true enabled: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface,
) { ) {
Preference( Preference(
title, title,
@ -114,6 +131,6 @@ fun Preference(
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} }
}, iconPadding, summary, onClick, controls, enabled }, iconPadding, summary, onClick, controls, enabled, containerColor,
) )
} }

View File

@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -15,25 +16,27 @@ fun PreferenceCategory(
iconPadding: Boolean = true, iconPadding: Boolean = true,
content: @Composable ColumnScope.() -> Unit content: @Composable ColumnScope.() -> Unit
) { ) {
Column { Column(
modifier = Modifier.fillMaxWidth(),
) {
if (title != null) { if (title != null) {
Row( Row(
modifier = Modifier 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( Text(
modifier = Modifier.padding(start = if (iconPadding) 56.dp else 0.dp),
text = title, text = title,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.secondary
) )
} }
} }
content() Column(
Box( modifier = Modifier
modifier = Modifier.fillMaxWidth().height(0.5.dp).background( .clip(MaterialTheme.shapes.medium),
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) verticalArrangement = Arrangement.spacedBy(2.dp)
) ) {
) content()
}
} }
} }

View File

@ -1,12 +1,13 @@
package de.mm20.launcher2.ui.component.preferences package de.mm20.launcher2.ui.component.preferences
import android.net.Uri import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut 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.RowScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding 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.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack 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.material.icons.rounded.HelpOutline
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -31,18 +33,18 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalViewConfiguration 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.locals.LocalNavController
import de.mm20.launcher2.ui.R
@Composable @Composable
@ -89,7 +91,7 @@ fun PreferenceScreen(
val touchSlop = LocalViewConfiguration.current.touchSlop val touchSlop = LocalViewConfiguration.current.touchSlop
var fabVisible by remember { mutableStateOf(true) } var fabVisible by remember { mutableStateOf(true) }
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
object: NestedScrollConnection { object : NestedScrollConnection {
override fun onPostScroll( override fun onPostScroll(
consumed: Offset, consumed: Offset,
available: Offset, available: Offset,
@ -102,7 +104,9 @@ fun PreferenceScreen(
} }
} }
val activity = LocalContext.current as? AppCompatActivity val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val activity = LocalActivity.current
Scaffold( Scaffold(
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( AnimatedVisibility(
@ -116,13 +120,20 @@ fun PreferenceScreen(
topBar = { topBar = {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = title, title = title,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
),
navigationIcon = { navigationIcon = {
IconButton(onClick = { IconButton(onClick = {
if (navController?.navigateUp() != true) { if (navController?.navigateUp() != true) {
activity?.onBackPressed() activity?.onBackPressed()
} }
}) { }) {
Icon(imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = "Back"
)
} }
}, },
actions = { actions = {
@ -135,25 +146,32 @@ fun PreferenceScreen(
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb()) .setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
.build() .build()
) )
.build().launchUrl(context, Uri.parse(helpUrl)) .build().launchUrl(context, helpUrl.toUri())
}) { }) {
Icon( Icon(
imageVector = Icons.Rounded.HelpOutline, imageVector = Icons.Rounded.HelpOutline,
contentDescription = "Help" contentDescription = stringResource(R.string.help)
) )
} }
} }
topBarActions() topBarActions()
} },
scrollBehavior = scrollBehavior,
) )
}) { },
containerColor = MaterialTheme.colorScheme.surfaceContainer
) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.padding(it), .padding(it),
state = lazyColumnState, state = lazyColumnState,
content = content, content = content,
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(
12.dp
)
) )
} }

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.component.preferences
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -20,10 +21,11 @@ fun PreferenceWithSwitch(
onClick: () -> Unit = {}, onClick: () -> Unit = {},
switchValue: Boolean, switchValue: Boolean,
onSwitchChanged: (Boolean) -> Unit, onSwitchChanged: (Boolean) -> Unit,
iconPadding: Boolean = true iconPadding: Boolean = icon != null
) { ) {
Row( Row(
verticalAlignment = (Alignment.CenterVertically) verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.extraSmall)
) { ) {
Box( Box(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.component.preferences package de.mm20.launcher2.ui.component.preferences
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -10,6 +11,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.floor import kotlin.math.floor
@ -20,6 +22,7 @@ import kotlin.math.roundToInt
fun SliderPreference( fun SliderPreference(
title: String, title: String,
icon: ImageVector? = null, icon: ImageVector? = null,
iconPadding: Boolean = icon != null,
value: Float, value: Float,
min: Float = 0f, min: Float = 0f,
max: Float = 1f, max: Float = 1f,
@ -33,25 +36,32 @@ fun SliderPreference(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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), .alpha(if (enabled) 1f else 0.38f),
) { ) {
Box( if (icon != null || iconPadding) {
modifier = Modifier Box(
.width(54.dp) modifier = Modifier
.padding(start = 4.dp), .width(56.dp)
contentAlignment = Alignment.CenterStart .padding(end = 8.dp),
) { contentAlignment = Alignment.Center
if (icon != null) { ) {
Icon( if (icon != null) {
imageVector = icon, Icon(
contentDescription = null, imageVector = icon,
tint = MaterialTheme.colorScheme.primary, contentDescription = null,
) tint = MaterialTheme.colorScheme.primary,
)
}
} }
} }
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
.padding(vertical = 16.dp)
) { ) {
Text( Text(
modifier = Modifier.padding(start = 2.dp), modifier = Modifier.padding(start = 2.dp),
@ -71,7 +81,7 @@ fun SliderPreference(
steps = step?.let { ((max - min) / it).toInt() - 1 } ?: 0, steps = step?.let { ((max - min) / it).toInt() - 1 } ?: 0,
onValueChangeFinished = { onValueChangeFinished = {
onValueChanged(sliderValue) onValueChanged(sliderValue)
} },
) )
if (label != null) { if (label != null) {
label(sliderValue) label(sliderValue)
@ -84,7 +94,8 @@ fun SliderPreference(
Text( Text(
modifier = Modifier.width(56.dp).padding(start = 24.dp), modifier = Modifier.width(56.dp).padding(start = 24.dp),
text = format.format(sliderValue), text = format.format(sliderValue),
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Center,
) )
} }
} }

View File

@ -1,18 +1,21 @@
package de.mm20.launcher2.ui.component.preferences package de.mm20.launcher2.ui.component.preferences
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
@Composable @Composable
fun SwitchPreference( fun SwitchPreference(
title: String, title: String,
icon: ImageVector? = null, icon: ImageVector? = null,
iconPadding: Boolean = true, iconPadding: Boolean = icon != null,
summary: String? = null, summary: String? = null,
value: Boolean, value: Boolean,
onValueChanged: (Boolean) -> Unit, onValueChanged: (Boolean) -> Unit,
enabled: Boolean = true enabled: Boolean = true,
containerColor: Color = MaterialTheme.colorScheme.surface,
) { ) {
Preference( Preference(
title = title, title = title,
@ -27,6 +30,7 @@ fun SwitchPreference(
Switch( Switch(
enabled = enabled, checked = value, onCheckedChange = onValueChanged, enabled = enabled, checked = value, onCheckedChange = onValueChanged,
) )
} },
containerColor = containerColor,
) )
} }

View File

@ -11,6 +11,7 @@ import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -132,7 +133,9 @@ class SettingsActivity : BaseActivity() {
) )
) )
} }
OverlayHost { OverlayHost(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceContainer)
) {
NavHost( NavHost(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
navController = navController, navController = navController,

View File

@ -21,30 +21,32 @@ fun BuildInfoSettingsScreen() {
val buildFeatures by viewModel.buildFeatures.collectAsState(emptyMap()) val buildFeatures by viewModel.buildFeatures.collectAsState(emptyMap())
PreferenceScreen(title = stringResource(R.string.preference_screen_buildinfo)) { PreferenceScreen(title = stringResource(R.string.preference_screen_buildinfo)) {
item { item {
Preference(title = "Build type", summary = BuildConfig.BUILD_TYPE) PreferenceCategory {
var buildSignature by remember { mutableStateOf<String?>(null) } Preference(title = "Build type", summary = BuildConfig.BUILD_TYPE)
LaunchedEffect(null) { var buildSignature by remember { mutableStateOf<String?>(null) }
val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { LaunchedEffect(null) {
val pi = context.packageManager.getPackageInfo( val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
context.packageName, val pi = context.packageManager.getPackageInfo(
PackageManager.GET_SIGNING_CERTIFICATES context.packageName,
) PackageManager.GET_SIGNING_CERTIFICATES
pi.signingInfo?.apkContentsSigners?.firstOrNull() )
} else { pi.signingInfo?.apkContentsSigners?.firstOrNull()
val pi = context.packageManager.getPackageInfo( } else {
context.packageName, val pi = context.packageManager.getPackageInfo(
PackageManager.GET_SIGNATURES context.packageName,
) PackageManager.GET_SIGNATURES
pi.signatures?.firstOrNull() )
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) { Preference(title = "Signature hash", summary = buildSignature)
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)
} }
item { item {
PreferenceCategory(title = "Features") { PreferenceCategory(title = "Features") {

View File

@ -6,8 +6,6 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ErrorOutline 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.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.ui.R 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.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.GuardedPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
@ -47,85 +45,81 @@ fun CalendarSearchSettingsScreen() {
PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) { PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) {
item { item {
PreferenceCategory { PreferenceCategory {
AnimatedVisibility(hasCalendarPermission == false) { GuardedPreference(
MissingPermissionBanner( locked = hasCalendarPermission == false,
text = stringResource(R.string.missing_permission_calendar_search_settings), onUnlock = {
onClick = { viewModel.requestCalendarPermission(context as AppCompatActivity)
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)
}, },
enabled = hasCalendarPermission == true, description = stringResource(R.string.missing_permission_calendar_search_settings),
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)
)
}
PreferenceWithSwitch( PreferenceWithSwitch(
title = stringResource(R.string.preference_search_tasks), title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_tasks_summary), summary = stringResource(R.string.preference_search_local_calendar_summary),
switchValue = enabledProviders.contains("tasks.org") && hasTasksPermission == true, switchValue = enabledProviders.contains("local") && hasCalendarPermission == true,
onSwitchChanged = { onSwitchChanged = {
viewModel.setProviderEnabled("tasks.org", it) viewModel.setProviderEnabled("local", it)
}, },
enabled = hasTasksPermission == true, enabled = hasCalendarPermission == true,
onClick = { onClick = {
navController?.navigate("settings/search/calendar/tasks.org") navController?.navigate("settings/search/calendar/local")
} }
) )
} }
for (plugin in plugins) { if (isTasksAppInstalled) {
val state = plugin.state GuardedPreference(
if (state is PluginState.SetupRequired) { locked = hasTasksPermission == false,
Banner( onUnlock = {
modifier = Modifier.padding(16.dp), viewModel.requestTasksPermission(context as AppCompatActivity)
text = state.message },
?: stringResource(id = R.string.plugin_state_setup_required), description = stringResource(R.string.missing_permission_tasks_search_settings),
icon = Icons.Rounded.ErrorOutline, ) {
primaryAction = { PreferenceWithSwitch(
TextButton(onClick = { title = stringResource(R.string.preference_search_tasks),
try { summary = stringResource(R.string.preference_search_tasks_summary),
state.setupActivity.sendWithBackgroundPermission(context) switchValue = enabledProviders.contains("tasks.org") && hasTasksPermission == true,
} catch (e: PendingIntent.CanceledException) { onSwitchChanged = {
CrashReporter.logException(e) viewModel.setProviderEnabled("tasks.org", it)
} },
}) { enabled = hasTasksPermission == true,
Text(stringResource(id = R.string.plugin_action_setup)) onClick = {
} navController?.navigate("settings/search/calendar/tasks.org")
} }
) )
} }
PreferenceWithSwitch( }
title = plugin.plugin.label, for (plugin in plugins) {
enabled = state is PluginState.Ready, val state = plugin.state
summary = (state as? PluginState.SetupRequired)?.message GuardedPreference(
?: (state as? PluginState.Ready)?.text locked = state is PluginState.SetupRequired,
?: plugin.plugin.description, onUnlock = {
switchValue = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready, try {
onSwitchChanged = { (state as PluginState.SetupRequired).setupActivity.sendWithBackgroundPermission(
viewModel.setProviderEnabled(plugin.plugin.authority, it) context
)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}, },
onClick = { description = (state as? PluginState.SetupRequired)?.message
navController?.navigate("settings/search/calendar/${plugin.plugin.authority}") ?: 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}")
}
)
}
} }
} }
} }

View File

@ -1,6 +1,8 @@
package de.mm20.launcher2.ui.settings.colorscheme package de.mm20.launcher2.ui.settings.colorscheme
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope 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.Icons
import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.DarkMode
import androidx.compose.material.icons.rounded.LightMode 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.ColorScheme
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
@ -23,6 +22,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
@ -35,12 +35,13 @@ fun ColorSchemePreferenceCategory(
preview: @Composable RowScope.() -> Unit preview: @Composable RowScope.() -> Unit
) { ) {
Column( Column(
modifier = Modifier.padding(top = 16.dp) modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .padding(start = 16.dp, bottom = 8.dp),
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( 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 MaterialTheme(
.fillMaxWidth() colorScheme = previewColorScheme
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainerLowest, MaterialTheme.shapes.extraSmall)
.horizontalScroll(rememberScrollState()) .horizontalScroll(rememberScrollState())
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -88,10 +90,8 @@ fun ColorSchemePreferenceCategory(
preview() preview()
} }
} }
colorPreferences()
} }
colorPreferences()
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
} }
} }

View File

@ -66,6 +66,7 @@ import de.mm20.launcher2.themes.merge
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.ShapedLauncherIcon 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.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalDarkTheme import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf
@ -142,17 +143,9 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
if (previewDarkTheme) DefaultDarkColorScheme else DefaultLightColorScheme if (previewDarkTheme) DefaultDarkColorScheme else DefaultLightColorScheme
item { item {
Column( PreferenceCategory(
modifier = Modifier 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( CorePaletteColorPreference(
title = "Primary", title = "Primary",
value = theme?.corePalette?.primary, value = theme?.corePalette?.primary,
@ -267,7 +260,6 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
} }
}, },
) )
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
} }
} }
item { item {
@ -681,7 +673,6 @@ fun ColorSchemeSettingsScreen(themeId: UUID) {
) )
}, },
defaultValue = selectedDefaultScheme.surfaceDim, defaultValue = selectedDefaultScheme.surfaceDim,
modifier = Modifier.padding(end = 12.dp),
) )
ThemeColorPreference( ThemeColorPreference(
title = "Surface", title = "Surface",

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.settings.colorscheme
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -28,6 +29,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -56,10 +58,12 @@ fun CorePaletteColorPreference(
Row( Row(
modifier = modifier.fillMaxWidth() modifier = modifier.fillMaxWidth()
.clip(MaterialTheme.shapes.extraSmall)
.clickable( .clickable(
onClick = { showDialog = true }, onClick = { showDialog = true },
) )
.padding(horizontal = 16.dp, vertical = 8.dp), .background(MaterialTheme.colorScheme.surface)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
ColorSwatch( ColorSwatch(

View File

@ -80,10 +80,12 @@ fun ThemeColorPreference(
Row( Row(
modifier = modifier.fillMaxWidth() modifier = modifier.fillMaxWidth()
.clip(MaterialTheme.shapes.extraSmall)
.clickable( .clickable(
onClick = { showDialog = true }, onClick = { showDialog = true },
) )
.padding(horizontal = 16.dp, vertical = 8.dp), .background(MaterialTheme.colorScheme.surface)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
ColorSwatch( ColorSwatch(

View File

@ -26,9 +26,9 @@ import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner 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.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@Composable @Composable
@ -38,7 +38,10 @@ fun ContactsSettingsScreen() {
val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null) val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null)
val hasCallPermission by viewModel.hasCallPermission.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 enabledProviders by viewModel.enabledProviders.collectAsState(emptySet())
val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(null) val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(null)
@ -56,69 +59,76 @@ fun ContactsSettingsScreen() {
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) )
} }
SwitchPreference( GuardedPreference(
title = stringResource(R.string.preference_search_contacts), locked = hasContactsPermission == false,
summary = stringResource(R.string.preference_search_contacts_summary), onUnlock = {
icon = Icons.Rounded.Person, viewModel.requestContactsPermission(context as AppCompatActivity)
value = enabledProviders.contains("local"), },
onValueChanged = { description = stringResource(R.string.missing_permission_contact_search_settings),
viewModel.setProviderEnabled("local", it) ) {
} 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) { for (plugin in plugins) {
val state = plugin.state val state = plugin.state
if (state is PluginState.SetupRequired) { GuardedPreference(
Banner( locked = state is PluginState.SetupRequired,
modifier = Modifier.padding(16.dp), onUnlock = {
text = state.message try {
?: stringResource(id = R.string.plugin_state_setup_required), (state as PluginState.SetupRequired).setupActivity.sendWithBackgroundPermission(
icon = Icons.Rounded.ErrorOutline, context
primaryAction = { )
TextButton(onClick = { } catch (e: PendingIntent.CanceledException) {
try { CrashReporter.logException(e)
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(id = R.string.plugin_action_setup))
}
} }
},
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 { PreferenceCategory {
AnimatedVisibility(hasCallPermission == false) { GuardedPreference(
MissingPermissionBanner( locked = hasCallPermission == false,
text = stringResource(R.string.missing_permission_call_contacts_settings), onUnlock = {
onClick = { viewModel.requestCallPermission(context as AppCompatActivity)
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
)
} }
} }
} }

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.settings.filesearch
import android.app.PendingIntent import android.app.PendingIntent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons 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.plugin.PluginState
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner 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.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@ -67,123 +66,104 @@ fun FileSearchSettingsScreen() {
PreferenceCategory { PreferenceCategory {
val localFiles by viewModel.localFiles.collectAsState() val localFiles by viewModel.localFiles.collectAsState()
val hasFilePermission by viewModel.hasFilePermission.collectAsState() val hasFilePermission by viewModel.hasFilePermission.collectAsState()
AnimatedVisibility(hasFilePermission == false) { GuardedPreference(
MissingPermissionBanner( locked = hasFilePermission == false,
text = stringResource( onUnlock = {
if (isAtLeastApiLevel(29)) R.string.missing_permission_file_search_settings_android10 else R.string.missing_permission_file_search_settings viewModel.requestFilePermission(context as AppCompatActivity)
), onClick = { },
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 nextcloud by viewModel.nextcloud.collectAsState()
val nextcloudAccount by viewModel.nextcloudAccount val nextcloudAccount by viewModel.nextcloudAccount
AnimatedVisibility(nextcloudAccount == null) { GuardedPreference(
Banner( locked = nextcloudAccount == null,
text = stringResource(R.string.no_account_nextcloud), onUnlock = {
icon = Icons.Rounded.AccountBox, viewModel.login(context as AppCompatActivity, AccountType.Nextcloud)
primaryAction = { },
TextButton(onClick = { icon = Icons.Rounded.AccountBox,
viewModel.login( description = stringResource(R.string.no_account_nextcloud),
context as AppCompatActivity, unlockLabel = stringResource(R.string.connect_account),
AccountType.Nextcloud ) {
) SwitchPreference(
}) { title = stringResource(R.string.preference_search_nextcloud),
Text( summary = nextcloudAccount?.let {
stringResource(R.string.connect_account), 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 owncloud by viewModel.owncloud.collectAsState()
val owncloudAccount by viewModel.owncloudAccount val owncloudAccount by viewModel.owncloudAccount
AnimatedVisibility(owncloudAccount == null) { GuardedPreference(
Banner( locked = owncloudAccount == null,
text = stringResource(R.string.no_account_owncloud), onUnlock = {
icon = Icons.Rounded.AccountBox, viewModel.login(context as AppCompatActivity, AccountType.Owncloud)
primaryAction = { },
TextButton(onClick = { icon = Icons.Rounded.AccountBox,
viewModel.login( description = stringResource(R.string.no_account_owncloud),
context as AppCompatActivity, unlockLabel = stringResource(R.string.connect_account),
AccountType.Owncloud ) {
) SwitchPreference(
}) { title = stringResource(R.string.preference_search_owncloud),
Text( summary = owncloudAccount?.let {
stringResource(R.string.connect_account), 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) { for (plugin in plugins) {
val state = plugin.state val state = plugin.state
if (state is PluginState.SetupRequired) { GuardedPreference(
Banner( locked = state is PluginState.SetupRequired,
modifier = Modifier.padding(16.dp), onUnlock = {
text = state.message ?: stringResource(id = R.string.plugin_state_setup_required), try {
icon = Icons.Rounded.ErrorOutline, (state as PluginState.SetupRequired).setupActivity.sendWithBackgroundPermission(
primaryAction = { context
TextButton(onClick = { )
try { } catch (e: PendingIntent.CanceledException) {
state.setupActivity.sendWithBackgroundPermission(context) CrashReporter.logException(e)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(id = R.string.plugin_action_setup))
}
} }
},
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)
},
)
} }
} }
} }

View File

@ -41,6 +41,7 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.common.SearchablePicker import de.mm20.launcher2.ui.common.SearchablePicker
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.ShapedLauncherIcon 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.ListPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@ -72,142 +73,137 @@ fun GestureSettingsScreen() {
val swipeDown by viewModel.swipeDown.collectAsStateWithLifecycle(null) 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 swipeDownApp by viewModel.swipeDownApp.collectAsState(null)
val swipeDownAppIcon by remember(swipeDownApp?.key) { val swipeDownAppIcon by remember(swipeDownApp?.key) {
viewModel.getIcon(swipeDownApp, appIconSize.toInt()) viewModel.getIcon(swipeDownApp, appIconSize.toInt())
}.collectAsState(null) }.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) GuardedPreference(
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeLeft)) { locked = hasPermission == false && requiresAccessibilityService(swipeDown),
MissingPermissionBanner( description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
modifier = Modifier.padding(16.dp), onUnlock = { viewModel.requestPermission(context as AppCompatActivity) },
text = stringResource(R.string.missing_permission_accessibility_gesture_settings), ) {
onClick = { 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 swipeLeftApp by viewModel.swipeLeftApp.collectAsState(null)
val swipeLeftAppIcon by remember(swipeLeftApp?.key) { val swipeLeftAppIcon by remember(swipeLeftApp?.key) {
viewModel.getIcon(swipeLeftApp, appIconSize.toInt()) viewModel.getIcon(swipeLeftApp, appIconSize.toInt())
}.collectAsState(null) }.collectAsState(null)
GesturePreference( GuardedPreference(
title = stringResource(R.string.preference_gesture_swipe_left), locked = hasPermission == false && requiresAccessibilityService(swipeLeft),
icon = Icons.Rounded.SwipeLeftAlt, description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
value = swipeLeft, onUnlock = { viewModel.requestPermission(context as AppCompatActivity) },
onValueChanged = { viewModel.setSwipeLeft(it) }, ) {
options = options, GesturePreference(
app = swipeLeftApp, title = stringResource(R.string.preference_gesture_swipe_left),
appIcon = swipeLeftAppIcon, icon = Icons.Rounded.SwipeLeftAlt,
onAppChanged = { viewModel.setSwipeLeftApp(it) } value = swipeLeft,
) onValueChanged = { viewModel.setSwipeLeft(it) },
options = options,
val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null) app = swipeLeftApp,
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeRight)) { appIcon = swipeLeftAppIcon,
MissingPermissionBanner( onAppChanged = { viewModel.setSwipeLeftApp(it) }
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
) )
} }
val swipeRight by viewModel.swipeRight.collectAsStateWithLifecycle(null)
val swipeRightApp by viewModel.swipeRightApp.collectAsState(null) val swipeRightApp by viewModel.swipeRightApp.collectAsState(null)
val swipeRightAppIcon by remember(swipeRightApp?.key) { val swipeRightAppIcon by remember(swipeRightApp?.key) {
viewModel.getIcon(swipeRightApp, appIconSize.toInt()) viewModel.getIcon(swipeRightApp, appIconSize.toInt())
}.collectAsState(null) }.collectAsState(null)
GesturePreference( GuardedPreference(
title = stringResource(R.string.preference_gesture_swipe_right), locked = hasPermission == false && requiresAccessibilityService(swipeRight),
icon = Icons.Rounded.SwipeRightAlt, description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
value = swipeRight, onUnlock = { viewModel.requestPermission(context as AppCompatActivity) },
onValueChanged = { viewModel.setSwipeRight(it) }, ) {
options = options, GesturePreference(
app = swipeRightApp, title = stringResource(R.string.preference_gesture_swipe_right),
appIcon = swipeRightAppIcon, icon = Icons.Rounded.SwipeRightAlt,
onAppChanged = { viewModel.setSwipeRightApp(it) } value = swipeRight,
) onValueChanged = { viewModel.setSwipeRight(it) },
options = options,
val swipeUp by viewModel.swipeUp.collectAsStateWithLifecycle(null) app = swipeRightApp,
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(swipeUp)) { appIcon = swipeRightAppIcon,
MissingPermissionBanner( onAppChanged = { viewModel.setSwipeRightApp(it) }
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
) )
} }
val swipeUp by viewModel.swipeUp.collectAsStateWithLifecycle(null)
val swipeUpApp by viewModel.swipeUpApp.collectAsState(null) val swipeUpApp by viewModel.swipeUpApp.collectAsState(null)
val swipeUpAppIcon by remember(swipeUpApp?.key) { val swipeUpAppIcon by remember(swipeUpApp?.key) {
viewModel.getIcon(swipeUpApp, appIconSize.toInt()) viewModel.getIcon(swipeUpApp, appIconSize.toInt())
}.collectAsState(null) }.collectAsState(null)
GesturePreference( GuardedPreference(
title = stringResource(R.string.preference_gesture_swipe_up), locked = hasPermission == false && requiresAccessibilityService(swipeUp),
icon = Icons.Rounded.SwipeUpAlt, description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
value = swipeUp, onUnlock = { viewModel.requestPermission(context as AppCompatActivity) },
onValueChanged = { viewModel.setSwipeUp(it) }, ) {
options = options, GesturePreference(
app = swipeUpApp, title = stringResource(R.string.preference_gesture_swipe_up),
appIcon = swipeUpAppIcon, icon = Icons.Rounded.SwipeUpAlt,
onAppChanged = { viewModel.setSwipeUpApp(it) } value = swipeUp,
) onValueChanged = { viewModel.setSwipeUp(it) },
options = options,
val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null) app = swipeUpApp,
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(doubleTap)) { appIcon = swipeUpAppIcon,
MissingPermissionBanner( onAppChanged = { viewModel.setSwipeUpApp(it) }
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
) )
} }
val doubleTap by viewModel.doubleTap.collectAsStateWithLifecycle(null)
val doubleTapApp by viewModel.doubleTapApp.collectAsState(null) val doubleTapApp by viewModel.doubleTapApp.collectAsState(null)
val doubleTapAppIcon by remember(doubleTapApp?.key) { val doubleTapAppIcon by remember(doubleTapApp?.key) {
viewModel.getIcon(doubleTapApp, appIconSize.toInt()) viewModel.getIcon(doubleTapApp, appIconSize.toInt())
}.collectAsState(null) }.collectAsState(null)
GesturePreference( GuardedPreference(
title = stringResource(R.string.preference_gesture_double_tap), locked = hasPermission == false && requiresAccessibilityService(doubleTap),
icon = Icons.Rounded.Adjust, description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
value = doubleTap, onUnlock = { viewModel.requestPermission(context as AppCompatActivity) },
onValueChanged = { viewModel.setDoubleTap(it) }, ) {
options = options, GesturePreference(
app = doubleTapApp, title = stringResource(R.string.preference_gesture_double_tap),
appIcon = doubleTapAppIcon, icon = Icons.Rounded.Adjust,
onAppChanged = { viewModel.setDoubleTapApp(it) } value = doubleTap,
) onValueChanged = { viewModel.setDoubleTap(it) },
options = options,
val longPress by viewModel.longPress.collectAsStateWithLifecycle(null) app = doubleTapApp,
AnimatedVisibility(hasPermission == false && requiresAccessibilityService(longPress)) { appIcon = doubleTapAppIcon,
MissingPermissionBanner( onAppChanged = { viewModel.setDoubleTapApp(it) }
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.missing_permission_accessibility_gesture_settings),
onClick = { viewModel.requestPermission(context as AppCompatActivity) }
) )
} }
val longPress by viewModel.longPress.collectAsStateWithLifecycle(null)
val longPressApp by viewModel.longPressApp.collectAsState(null) val longPressApp by viewModel.longPressApp.collectAsState(null)
val longPressAppIcon by remember(longPressApp?.key) { val longPressAppIcon by remember(longPressApp?.key) {
viewModel.getIcon(longPressApp, appIconSize.toInt()) viewModel.getIcon(longPressApp, appIconSize.toInt())
}.collectAsState(null) }.collectAsState(null)
GesturePreference( GuardedPreference(
title = stringResource(R.string.preference_gesture_long_press), locked = hasPermission == false && requiresAccessibilityService(longPress),
icon = Icons.Rounded.Circle, description = stringResource(R.string.missing_permission_accessibility_gesture_settings),
value = longPress, onUnlock = { viewModel.requestPermission(context as AppCompatActivity) },
onValueChanged = { viewModel.setLongPress(it) }, ) {
options = options, GesturePreference(
app = longPressApp, title = stringResource(R.string.preference_gesture_long_press),
appIcon = longPressAppIcon, icon = Icons.Rounded.Circle,
onAppChanged = { viewModel.setLongPressApp(it) } value = longPress,
) onValueChanged = { viewModel.setLongPress(it) },
options = options,
app = longPressApp,
appIcon = longPressAppIcon,
onAppChanged = { viewModel.setLongPressApp(it) }
)
}
} }
} }
} }

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.settings.icons
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -144,24 +143,20 @@ fun IconsSettingsScreen() {
item { item {
PreferenceCategory(stringResource(R.string.preference_category_icons)) { PreferenceCategory(stringResource(R.string.preference_category_icons)) {
if (previewIcons.value.isNotEmpty()) { if (previewIcons.value.isNotEmpty()) {
Surface( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .background(
.padding(16.dp), MaterialTheme.colorScheme.surfaceContainerLowest,
shape = MaterialTheme.shapes.medium, MaterialTheme.shapes.extraSmall
color = MaterialTheme.colorScheme.surfaceContainer, )
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant) .padding(vertical = 24.dp, horizontal = 8.dp)
) { ) {
Row( for (icon in previewIcons.value) {
modifier = Modifier.padding(vertical = 24.dp, horizontal = 8.dp) Box(
) { modifier = Modifier.weight(1f),
for (icon in previewIcons.value) { contentAlignment = Alignment.Center
Box( ) {
modifier = Modifier.weight(1f), ShapedLauncherIcon(size = grid.iconSize.dp, icon = { icon })
contentAlignment = Alignment.Center
) {
ShapedLauncherIcon(size = grid.iconSize.dp, icon = { icon })
}
} }
} }
} }
@ -445,7 +440,8 @@ fun IconShapePreference(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Box( Box(
modifier = Modifier.clip(getShape(it)) modifier = Modifier
.clip(getShape(it))
.size(48.dp) .size(48.dp)
.background(MaterialTheme.colorScheme.primary) .background(MaterialTheme.colorScheme.primary)
.clickable { .clickable {

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.settings.nextcloud package de.mm20.launcher2.ui.settings.nextcloud
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border 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.material.icons.automirrored.rounded.Logout
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -24,7 +24,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -33,6 +32,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference 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.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalNavController import de.mm20.launcher2.ui.locals.LocalNavController
@ -60,10 +60,12 @@ fun NextcloudSettingsScreen() {
item { item {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer) .background(
MaterialTheme.colorScheme.secondaryContainer,
MaterialTheme.shapes.medium
)
.fillParentMaxWidth() .fillParentMaxWidth()
.padding(vertical = 64.dp) .padding(vertical = 64.dp),
,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@ -72,7 +74,11 @@ fun NextcloudSettingsScreen() {
modifier = Modifier modifier = Modifier
.size(72.dp) .size(72.dp)
.background(MaterialTheme.colorScheme.secondary, CircleShape) .background(MaterialTheme.colorScheme.secondary, CircleShape)
.border(2.dp, MaterialTheme.colorScheme.onSecondaryContainer, CircleShape), .border(
2.dp,
MaterialTheme.colorScheme.onSecondaryContainer,
CircleShape
),
) { ) {
Text( Text(
text = nextcloudUser!!.userName.split(" ") text = nextcloudUser!!.userName.split(" ")
@ -80,15 +86,17 @@ fun NextcloudSettingsScreen() {
.joinToString("").let { .joinToString("").let {
if (it.length >= 2) it.first().toString() + it.last().toString() if (it.length >= 2) it.first().toString() + it.last().toString()
else it.first().toString() else it.first().toString()
} },
,
color = MaterialTheme.colorScheme.onSecondary, color = MaterialTheme.colorScheme.onSecondary,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
) )
} }
Text( Text(
modifier = Modifier.padding(top = 24.dp), 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, color = MaterialTheme.colorScheme.onSecondaryContainer,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
@ -104,40 +112,42 @@ fun NextcloudSettingsScreen() {
modifier = Modifier modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing) .padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize), .size(ButtonDefaults.IconSize),
contentDescription = null) contentDescription = null
)
Text(text = stringResource(R.string.preference_signout)) Text(text = stringResource(R.string.preference_signout))
} }
} }
} }
item {
HorizontalDivider()
}
item { item {
SwitchPreference( PreferenceCategory {
title = stringResource(R.string.plugin_type_filesearch), SwitchPreference(
summary = stringResource(R.string.preference_search_cloud_summary, nextcloudUser!!.userName), title = stringResource(R.string.plugin_type_filesearch),
value = searchFiles == true, summary = stringResource(
onValueChanged = { R.string.preference_search_cloud_summary,
viewModel.setSearchFiles(it) nextcloudUser!!.userName
}, ),
iconPadding = false, value = searchFiles == true,
) onValueChanged = {
viewModel.setSearchFiles(it)
},
iconPadding = false,
)
}
} }
} else { } else {
item { item {
val activity = LocalContext.current as AppCompatActivity val activity = LocalActivity.current as AppCompatActivity
Preference( PreferenceCategory {
title = stringResource(R.string.preference_nextcloud_signin), Preference(
summary = stringResource(R.string.preference_nextcloud_signin_summary), title = stringResource(R.string.preference_nextcloud_signin),
icon = Icons.AutoMirrored.Rounded.Login, summary = stringResource(R.string.preference_nextcloud_signin_summary),
onClick = { icon = Icons.AutoMirrored.Rounded.Login,
viewModel.signIn(activity) onClick = {
} viewModel.signIn(activity)
) }
} )
item { }
HorizontalDivider()
} }
} }
} }

View File

@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.settings.owncloud package de.mm20.launcher2.ui.settings.owncloud
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -33,6 +34,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference 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.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalNavController import de.mm20.launcher2.ui.locals.LocalNavController
@ -60,7 +62,7 @@ fun OwncloudSettingsScreen() {
item { item {
Column( Column(
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer) .background(MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.shapes.medium)
.fillParentMaxWidth() .fillParentMaxWidth()
.padding(vertical = 64.dp) .padding(vertical = 64.dp)
, ,
@ -109,35 +111,36 @@ fun OwncloudSettingsScreen() {
} }
} }
} }
item {
HorizontalDivider()
}
item { item {
SwitchPreference( PreferenceCategory {
title = stringResource(R.string.plugin_type_filesearch), SwitchPreference(
summary = stringResource(R.string.preference_search_cloud_summary, owncloudUser!!.userName), title = stringResource(R.string.plugin_type_filesearch),
value = searchFiles == true, summary = stringResource(
onValueChanged = { R.string.preference_search_cloud_summary,
viewModel.setSearchFiles(it) owncloudUser!!.userName
}, ),
iconPadding = false, value = searchFiles == true,
) onValueChanged = {
viewModel.setSearchFiles(it)
},
iconPadding = false,
)
}
} }
} else { } else {
item { item {
val activity = LocalContext.current as AppCompatActivity val activity = LocalActivity.current as AppCompatActivity
Preference( PreferenceCategory {
title = stringResource(R.string.preference_owncloud_signin), Preference(
summary = stringResource(R.string.preference_owncloud_signin_summary), title = stringResource(R.string.preference_owncloud_signin),
icon = Icons.AutoMirrored.Rounded.Login, summary = stringResource(R.string.preference_owncloud_signin_summary),
onClick = { icon = Icons.AutoMirrored.Rounded.Login,
viewModel.signIn(activity) onClick = {
} viewModel.signIn(activity)
) }
} )
item { }
HorizontalDivider()
} }
} }
} }

View File

@ -7,7 +7,7 @@ import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -34,8 +34,8 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -59,8 +59,8 @@ import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.themes.atTone import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R 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.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.GuardedPreference
import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
@ -142,6 +142,10 @@ fun PluginSettingsScreen(pluginId: String) {
topBar = { topBar = {
TopAppBar( TopAppBar(
title = {}, title = {},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
),
navigationIcon = { navigationIcon = {
IconButton(onClick = { IconButton(onClick = {
if (navController?.navigateUp() != true) { if (navController?.navigateUp() != true) {
@ -186,7 +190,8 @@ fun PluginSettingsScreen(pluginId: String) {
} }
} }
) )
} },
containerColor = MaterialTheme.colorScheme.surfaceContainer,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -194,10 +199,14 @@ fun PluginSettingsScreen(pluginId: String) {
.padding(it) .padding(it)
) { ) {
Surface( Surface(
modifier = Modifier.fillMaxWidth() modifier = Modifier
.padding(bottom = 16.dp) .fillMaxWidth()
.padding(bottom = 16.dp),
color = MaterialTheme.colorScheme.surfaceContainer,
) { ) {
Column { Column(
modifier = Modifier.padding(bottom = 16.dp)
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp) modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp)
@ -253,327 +262,60 @@ fun PluginSettingsScreen(pluginId: String) {
} }
} }
val surfaceColor by animateColorAsState( Column(
if (pluginPackage?.enabled == true) { modifier = Modifier.padding(horizontal = 12.dp),
MaterialTheme.colorScheme.secondaryContainer verticalArrangement = Arrangement.spacedBy(12.dp),
} else {
MaterialTheme.colorScheme.surfaceContainer
}
)
Surface(
modifier = Modifier
.fillMaxWidth(),
color = surfaceColor,
) { ) {
SwitchPreference( PreferenceCategory {
enabled = pluginPackage != null && hasPermission != null, SwitchPreference(
iconPadding = false, enabled = pluginPackage != null && hasPermission != null,
title = stringResource(R.string.preference_plugin_enable), iconPadding = false,
value = pluginPackage?.enabled == true && hasPermission == true, title = stringResource(R.string.preference_plugin_enable),
onValueChanged = { value = pluginPackage?.enabled == true && hasPermission == true,
if (hasPermission == true) { onValueChanged = {
viewModel.setPluginEnabled(it) if (hasPermission == true) {
} else { viewModel.setPluginEnabled(it)
requestPermissionStarter.launch( } else {
Intent().apply { requestPermissionStarter.launch(
`package` = pluginPackage?.packageName Intent().apply {
action = "de.mm20.launcher2.plugin.REQUEST_PERMISSION" `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
} }
) )
if (showDialog && calendarLists?.isNotEmpty() == true) { }
ModalBottomSheet( },
onDismissRequest = { containerColor = MaterialTheme.colorScheme.secondaryContainer
showDialog = false )
}, }
) { AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) {
LazyColumn { Column(
items(calendarLists!!) { modifier = Modifier.verticalScroll(rememberScrollState())
CheckboxPreference( ) {
title = it.name, if (filePlugins.isNotEmpty()) {
summary = it.owner, PreferenceCategory(
iconPadding = false, stringResource(R.string.plugin_type_filesearch),
value = it.id !in excludedCalendars, iconPadding = false,
onValueChanged = { value -> ) {
viewModel.setCalendarExcluded(it.id, !value) 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( is PluginState.SetupRequired -> {
checkedColor = if (it.color == 0) MaterialTheme.colorScheme.primary state.message
else Color( ?: stringResource(R.string.plugin_state_setup_required)
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)
)
)
)
} }
}
} else -> ""
} },
} onUnlock = if (state is PluginState.SetupRequired) {
} {
}
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 = {
try { try {
state.setupActivity.sendWithBackgroundPermission( state.setupActivity.sendWithBackgroundPermission(
context context
@ -581,40 +323,278 @@ fun PluginSettingsScreen(pluginId: String) {
} catch (e: PendingIntent.CanceledException) { } catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e) 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
)
})
} }
) GuardedPreference(
} else if (state is PluginState.Error) { locked = state is PluginState.Error || state is PluginState.SetupRequired,
Banner( icon = if (state is PluginState.Error) Icons.Rounded.Error else Icons.Rounded.Info,
modifier = Modifier.padding(16.dp), description = when (state) {
text = stringResource(R.string.plugin_state_error), is PluginState.Error -> {
icon = Icons.Rounded.Error, stringResource(R.string.plugin_state_error)
color = MaterialTheme.colorScheme.errorContainer, }
)
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( Preference(
title = plugin.plugin.label, title = stringResource(R.string.widget_config_weather_integration_settings),
enabled = state is PluginState.Ready && weatherProviderId != plugin.plugin.authority, icon = Icons.AutoMirrored.Rounded.OpenInNew,
iconPadding = false,
summary = if (weatherProviderId != plugin.plugin.authority) {
stringResource(R.string.plugin_weather_provider_enable)
} else {
stringResource(R.string.plugin_weather_provider_enabled)
},
onClick = { 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")
}
)
} }
} }
} }

View File

@ -31,7 +31,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.Wikipedia 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.BottomSheetDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.SmallMessage 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.ListPreference
import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory 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 hasContactPlugins by remember { derivedStateOf { plugins?.any { it.plugin.type == PluginType.ContactSearch } } }
val isTasksAppInstalled by viewModel.isTasksAppInstalled.collectAsStateWithLifecycle() 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 hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null)
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle(null) val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle(null)
val hasLocationPermission by viewModel.hasLocationPermission.collectAsStateWithLifecycle(null) val hasLocationPermission by viewModel.hasLocationPermission.collectAsStateWithLifecycle(null)
@ -121,28 +123,27 @@ fun SearchSettingsScreen() {
}, },
) )
} else { } else {
AnimatedVisibility(hasContactsPermission == false) { GuardedPreference(
MissingPermissionBanner( locked = hasContactsPermission == false,
text = stringResource(R.string.missing_permission_contact_search_settings), onUnlock = {
onClick = { viewModel.requestContactsPermission(context as AppCompatActivity)
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) { if (hasCalendarPlugins != false || isTasksAppInstalled != false) {
@ -155,51 +156,50 @@ fun SearchSettingsScreen() {
}, },
) )
} else { } else {
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner( GuardedPreference(
text = stringResource(R.string.missing_permission_calendar_search_settings), locked = hasCalendarPermission == false,
onClick = { onUnlock = {
viewModel.requestCalendarPermission(context as AppCompatActivity) 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) { GuardedPreference(
MissingPermissionBanner( locked = hasAppShortcutsPermission == false,
text = stringResource( onUnlock = {
R.string.missing_permission_appshortcuts_search_settings, viewModel.requestAppShortcutsPermission(context as AppCompatActivity)
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)
}, },
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( SwitchPreference(
title = stringResource(R.string.preference_search_calculator), title = stringResource(R.string.preference_search_calculator),
@ -246,43 +246,38 @@ fun SearchSettingsScreen() {
viewModel.setWebsites(it) viewModel.setWebsites(it)
} }
) )
GuardedPreference(
AnimatedVisibility(hasLocationPermission == false) { locked = hasLocationPermission == false,
MissingPermissionBanner( onUnlock = {
text = stringResource( viewModel.requestLocationPermission(context as AppCompatActivity)
R.string.missing_permission_location_search, },
), description = stringResource(R.string.missing_permission_location_search),
onClick = { ) {
viewModel.requestLocationPermission(context as AppCompatActivity) if (hasLocationPlugins != false) {
}, Preference(
modifier = Modifier.padding(16.dp) title = stringResource(R.string.preference_search_locations),
) summary = stringResource(R.string.preference_search_locations_summary),
} icon = Icons.Rounded.Place,
enabled = hasLocationPermission == true,
if (hasLocationPlugins != false) { onClick = {
Preference( navController?.navigate("settings/search/locations")
title = stringResource(R.string.preference_search_locations), }
summary = stringResource(R.string.preference_search_locations_summary), )
icon = Icons.Rounded.Place, } else {
enabled = hasLocationPermission == true, PreferenceWithSwitch(
onClick = { title = stringResource(R.string.preference_search_locations),
navController?.navigate("settings/search/locations") summary = stringResource(R.string.preference_search_locations_summary),
} icon = Icons.Rounded.Place,
) onClick = {
} else { navController?.navigate("settings/search/locations")
PreferenceWithSwitch( },
title = stringResource(R.string.preference_search_locations), switchValue = places == true,
summary = stringResource(R.string.preference_search_locations_summary), onSwitchChanged = {
icon = Icons.Rounded.Place, viewModel.setPlacesSearch(it)
onClick = { },
navController?.navigate("settings/search/locations") enabled = hasLocationPermission == true,
}, )
switchValue = places == true, }
onSwitchChanged = {
viewModel.setPlacesSearch(it)
},
enabled = hasLocationPermission == true,
)
} }
Preference( Preference(
@ -327,6 +322,7 @@ fun SearchSettingsScreen() {
) )
SwitchPreference( SwitchPreference(
title = stringResource(R.string.preference_filter_bar), title = stringResource(R.string.preference_filter_bar),
iconPadding = true,
summary = stringResource(R.string.preference_filter_bar_summary), summary = stringResource(R.string.preference_filter_bar_summary),
value = filterBar == true, value = filterBar == true,
onValueChanged = { onValueChanged = {
@ -336,6 +332,7 @@ fun SearchSettingsScreen() {
AnimatedVisibility(filterBar == true) { AnimatedVisibility(filterBar == true) {
Preference( Preference(
title = stringResource(R.string.preference_customize_filter_bar), title = stringResource(R.string.preference_customize_filter_bar),
iconPadding = true,
summary = stringResource(R.string.preference_customize_filter_bar_summary), summary = stringResource(R.string.preference_customize_filter_bar_summary),
onClick = { onClick = {
navController?.navigate("settings/search/filterbar") navController?.navigate("settings/search/filterbar")
@ -357,6 +354,7 @@ fun SearchSettingsScreen() {
) )
SwitchPreference( SwitchPreference(
title = stringResource(R.string.preference_search_bar_launch_on_enter), title = stringResource(R.string.preference_search_bar_launch_on_enter),
iconPadding = true,
summary = stringResource(R.string.preference_search_bar_launch_on_enter_summary), summary = stringResource(R.string.preference_search_bar_launch_on_enter_summary),
value = launchOnEnter == true, value = launchOnEnter == true,
onValueChanged = { onValueChanged = {

View File

@ -29,8 +29,6 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
@ -53,6 +51,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -222,6 +221,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) {
} }
) )
} }
}
item {
PreferenceCategory { PreferenceCategory {
ShapePreview( ShapePreview(
previewShapes = previewShapes, previewShapes = previewShapes,
@ -246,6 +247,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) {
} }
) )
} }
}
item {
PreferenceCategory { PreferenceCategory {
ShapePreview( ShapePreview(
previewShapes = previewShapes, previewShapes = previewShapes,
@ -279,7 +282,9 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) {
} }
) )
} }
PreferenceCategory { }
item {
PreferenceCategory(title = "Large") {
ShapePreview( ShapePreview(
previewShapes = previewShapes, previewShapes = previewShapes,
) { ) {
@ -299,6 +304,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) {
} }
) )
} }
}
item {
PreferenceCategory { PreferenceCategory {
ShapePreference( ShapePreference(
title = "Large increased", title = "Large increased",
@ -312,6 +319,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) {
} }
) )
} }
}
item {
PreferenceCategory { PreferenceCategory {
ShapePreview( ShapePreview(
previewShapes = previewShapes, previewShapes = previewShapes,
@ -344,6 +353,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) {
} }
) )
} }
}
item {
PreferenceCategory { PreferenceCategory {
ShapePreference( ShapePreference(
title = "Extra large increased", title = "Extra large increased",
@ -357,6 +368,8 @@ fun ShapeSchemeSettingsScreen(themeId: UUID) {
} }
) )
} }
}
item {
PreferenceCategory { PreferenceCategory {
ShapePreference( ShapePreference(
title = "Extra extra large", title = "Extra extra large",
@ -398,11 +411,12 @@ fun ShapePreference(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 8.dp) .clip(MaterialTheme.shapes.extraSmall)
.background(MaterialTheme.colorScheme.surface)
.clickable( .clickable(
onClick = { showDialog = true }, onClick = { showDialog = true },
) )
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Box( Box(
@ -706,28 +720,20 @@ private fun ShapePreview(
previewShapes: Shapes, previewShapes: Shapes,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
MaterialTheme( Row(
shapes = previewShapes 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( MaterialTheme(
modifier = Modifier shapes = previewShapes
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer
)
) { ) {
content()
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
content()
}
} }
} }
} }

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.settings.tasks
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.OpenInNew 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.Info
import androidx.compose.material.icons.rounded.TaskAlt import androidx.compose.material.icons.rounded.TaskAlt
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -19,7 +21,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner 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.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@ -41,55 +43,63 @@ fun TasksIntegrationSettingsScreen() {
) { ) {
if (isTasksInstalled == false) { if (isTasksInstalled == false) {
item { item {
Banner( PreferenceCategory {
text = stringResource( Banner(
R.string.preference_tasks_integration_description, text = stringResource(
stringResource(R.string.app_name) R.string.preference_tasks_integration_description,
), stringResource(R.string.app_name)
icon = Icons.Rounded.Info, ),
modifier = Modifier.padding(16.dp), icon = Icons.Rounded.Info,
primaryAction = { modifier = Modifier
Button(onClick = { .background(MaterialTheme.colorScheme.surface)
viewModel.downloadTasksApp(activity as AppCompatActivity) .padding(16.dp),
}) { primaryAction = {
Text(stringResource(R.string.action_install)) Button(onClick = {
viewModel.downloadTasksApp(activity as AppCompatActivity)
}) {
Text(stringResource(R.string.action_install))
}
} }
} )
) }
} }
} }
if (isTasksInstalled == true) { if (isTasksInstalled == true) {
item { item {
PreferenceCategory { 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) { if (hasTasksPermission == true) {
Banner( Banner(
text = stringResource(R.string.preference_tasks_integration_ready), text = stringResource(R.string.preference_tasks_integration_ready),
icon = Icons.Rounded.CheckCircle, icon = Icons.Rounded.CheckCircle,
modifier = Modifier.padding(16.dp), modifier = Modifier
.background(
MaterialTheme.colorScheme.surface,
MaterialTheme.shapes.extraSmall
)
.padding(16.dp),
) )
} }
PreferenceWithSwitch( GuardedPreference(
icon = Icons.Rounded.TaskAlt, locked = hasTasksPermission == false,
title = stringResource(R.string.preference_search_tasks), onUnlock = {
summary = stringResource(R.string.preference_search_tasks_summary), viewModel.requestTasksPermission(activity as AppCompatActivity)
switchValue = isTasksSearchEnabled == true && hasTasksPermission == true,
onSwitchChanged = {
viewModel.setTasksSearchEnabled(it)
}, },
enabled = hasTasksPermission == true, description = stringResource(R.string.missing_permission_tasks_integration),
onClick = { ) {
navController?.navigate("settings/search/calendar/tasks.org") 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( Preference(
title = stringResource(R.string.preference_launch_tasks_app), title = stringResource(R.string.preference_launch_tasks_app),
icon = Icons.AutoMirrored.Rounded.OpenInNew, icon = Icons.AutoMirrored.Rounded.OpenInNew,

View File

@ -45,6 +45,8 @@ fun UnitConverterSettingsScreen() {
} }
) )
} }
}
item {
PreferenceCategory { PreferenceCategory {
Preference( Preference(
title = stringResource(R.string.preference_search_supportedunits), title = stringResource(R.string.preference_search_supportedunits),

View File

@ -10,19 +10,34 @@ fun makeTypography(
val baseTypography = Typography() val baseTypography = Typography()
return Typography( return Typography(
displayLarge = baseTypography.displayLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium), 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), 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), 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), 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), 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), 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), 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), titleSmall = baseTypography.titleSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold),
titleSmallEmphasized = baseTypography.titleSmallEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold),
bodyLarge = baseTypography.bodyLarge.copy(fontFamily = bodyFamily), bodyLarge = baseTypography.bodyLarge.copy(fontFamily = bodyFamily),
bodyLargeEmphasized = baseTypography.bodyLargeEmphasized.copy(fontFamily = bodyFamily, fontWeight = FontWeight.Medium),
bodyMedium = baseTypography.bodyMedium.copy(fontFamily = bodyFamily), bodyMedium = baseTypography.bodyMedium.copy(fontFamily = bodyFamily),
bodyMediumEmphasized = baseTypography.bodyMediumEmphasized.copy(fontFamily = bodyFamily, fontWeight = FontWeight.Medium),
bodySmall = baseTypography.bodySmall.copy(fontFamily = bodyFamily), bodySmall = baseTypography.bodySmall.copy(fontFamily = bodyFamily),
labelLarge = baseTypography.labelLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), bodySmallEmphasized = baseTypography.bodySmallEmphasized.copy(fontFamily = bodyFamily, fontWeight = FontWeight.Medium),
labelMedium = baseTypography.labelMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold), labelLarge = baseTypography.labelLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Medium),
labelSmall = baseTypography.labelSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold) 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),
) )
} }

View File

@ -11,6 +11,7 @@ val Outfit = FontFamily(
Font(R.font.outfit_200, weight = FontWeight.ExtraLight, style = FontStyle.Normal), 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_300, weight = FontWeight.Light, style = FontStyle.Normal),
Font(R.font.outfit_400, weight = FontWeight.Normal, 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_600, weight = FontWeight.SemiBold, style = FontStyle.Normal),
Font(R.font.outfit_700, weight = FontWeight.Bold, 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), Font(R.font.outfit_800, weight = FontWeight.ExtraBold, style = FontStyle.Normal),