Merge branch 'main' into Hide-zombie-secure-folder-tab

This commit is contained in:
MM20
2025-04-02 20:46:12 +02:00
78 changed files with 5707 additions and 2094 deletions
+5
View File
@@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission android:name="android.permission.SET_ALARM" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
@@ -23,6 +27,7 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.VIBRATE" />
@@ -13,7 +13,6 @@ import de.mm20.launcher2.widgets.FavoritesWidget
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.koin.androidx.compose.inject
@Composable
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
@@ -21,6 +22,7 @@ fun BottomSheetDialog(
content: @Composable (paddingValues: PaddingValues) -> Unit,
) {
ModalBottomSheet(
modifier = Modifier.statusBarsPadding().padding(top = 8.dp),
sheetState = bottomSheetState,
onDismissRequest = onDismissRequest,
) {
@@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.ktx
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import hct.Hct
import kotlin.math.atan2
import kotlin.math.roundToInt
@@ -20,6 +21,14 @@ fun Color.Companion.hct(hue: Float, chroma: Float, tone: Float): Color {
return Color(hct.toInt())
}
fun Color.atTone(tone: Int): Color {
return Color(
Hct.fromInt(this.toArgb()).apply {
this.tone = tone.toDouble()
}.toInt()
)
}
val Color.hue: Float
get() {
val r = this.red / 255f
@@ -28,3 +37,5 @@ val Color.hue: Float
// sqrt(3)
return atan2(1.7320508f * (g - b), 2f * r - g - b)
}
fun android.graphics.Color.toComposeColor() = Color(this.toArgb())
@@ -64,6 +64,7 @@ fun SearchColumn(
) {
val columns = LocalGridSettings.current.columnCount
val showList = LocalGridSettings.current.showList
val context = LocalContext.current
val viewModel: SearchVM = viewModel()
@@ -93,6 +94,7 @@ fun SearchColumn(
val bestMatch by viewModel.bestMatch
val query by viewModel.searchQuery
val isSearchEmpty by viewModel.isSearchEmpty
val missingCalendarPermission by viewModel.missingCalendarPermission.collectAsState(false)
@@ -111,13 +113,14 @@ fun SearchColumn(
val expandedCategory: SearchCategory? by viewModel.expandedCategory
var selectedAppProfileIndex: Int by remember(isSearchEmpty) { mutableIntStateOf(0) }
var selectedContactIndex: Int by remember(contacts) { mutableIntStateOf(-1) }
var selectedFileIndex: Int by remember(files) { mutableIntStateOf(-1) }
var selectedCalendarIndex: Int by remember(events) { mutableIntStateOf(-1) }
var selectedLocationIndex: Int by remember(locations) { mutableIntStateOf(-1) }
var selectedShortcutIndex: Int by remember(appShortcuts) { mutableIntStateOf(-1) }
var selectedArticleIndex: Int by remember(wikipedia) { mutableIntStateOf(-1) }
var selectedWebsiteIndex: Int by remember(website) { mutableIntStateOf(-1) }
var selectedAppIndex: Int by remember(query) { mutableIntStateOf(-1) }
var selectedContactIndex: Int by remember(query) { mutableIntStateOf(-1) }
var selectedFileIndex: Int by remember(query) { mutableIntStateOf(-1) }
var selectedCalendarIndex: Int by remember(query) { mutableIntStateOf(-1) }
var selectedLocationIndex: Int by remember(query) { mutableIntStateOf(-1) }
var selectedShortcutIndex: Int by remember(query) { mutableIntStateOf(-1) }
var selectedArticleIndex: Int by remember(query) { mutableIntStateOf(-1) }
var selectedWebsiteIndex: Int by remember(query) { mutableIntStateOf(-1) }
val showFilters by viewModel.showFilters
@@ -193,6 +196,9 @@ fun SearchColumn(
columns = columns,
reverse = reverse,
showProfileLockControls = hasProfilesPermission,
showList = showList,
selectedIndex = selectedAppIndex,
onSelect = { selectedAppIndex = it },
)
} else {
AppResults(
@@ -202,7 +208,10 @@ fun SearchColumn(
selectedAppProfileIndex = it
},
columns = columns,
reverse = reverse
reverse = reverse,
showList = showList,
selectedIndex = selectedAppIndex,
onSelect = { selectedAppIndex = it },
)
}
@@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.Intent
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
@@ -61,7 +62,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.roundToIntRect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import de.mm20.launcher2.crashreporter.CrashReporter
@@ -85,11 +85,15 @@ import kotlinx.coroutines.launch
fun AppItem(
modifier: Modifier = Modifier,
app: Application,
showDetails: Boolean,
onBack: () -> Unit
) {
val viewModel: SearchableItemVM = listItemViewModel(key = "search-${app.key}")
val iconSize = LocalGridSettings.current.iconSize.dp.toPixels()
val badge by viewModel.badge.collectAsStateWithLifecycle(null)
val icon by viewModel.icon.collectAsStateWithLifecycle()
LaunchedEffect(app) {
viewModel.init(app, iconSize.toInt())
}
@@ -97,386 +101,440 @@ fun AppItem(
val context = LocalContext.current
val scope = rememberCoroutineScope()
Column(
modifier = modifier.verticalScroll(rememberScrollState())
) {
Row {
Column(
modifier = Modifier
.weight(1f)
.padding(16.dp)
) {
Text(
text = app.labelOverride ?: app.label,
style = MaterialTheme.typography.titleMedium
)
if (!app.isPrivate) {
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
text = tags.joinToString(separator = " #", prefix = "#"),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.labelSmall
)
}
app.versionName?.let {
Text(
text = stringResource(R.string.app_info_version, it),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = app.componentName.packageName,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 1.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Text(
stringResource(R.string.profile_private_profile_state_locked),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp),
color = MaterialTheme.colorScheme.secondary,
)
}
}
val badge by viewModel.badge.collectAsStateWithLifecycle(null)
val icon by viewModel.icon.collectAsStateWithLifecycle()
ShapedLauncherIcon(
size = 48.dp,
modifier = Modifier
.padding(16.dp),
badge = { badge },
icon = { icon },
)
}
val notifications by viewModel.notifications.collectAsState(emptyList())
AnimatedVisibility(notifications.isNotEmpty()) {
var showAllNotifications by remember { mutableStateOf(false) }
AnimatedContent(
showAllNotifications || notifications.size == 1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.border(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
MaterialTheme.shapes.small
)
.clip(MaterialTheme.shapes.small)
) { showAll ->
if (showAll) {
Column(
modifier = Modifier.animateContentSize()
) {
for ((i, not) in notifications.withIndex()) {
val icon =
remember(not.smallIcon) { not.smallIcon?.loadDrawable(context) }
if (not.title == null && not.text == null) continue
if (i > 0) {
HorizontalDivider()
}
Row(
verticalAlignment = Alignment.CenterVertically,
SharedTransitionLayout(modifier = modifier) {
AnimatedContent(showDetails) { showDetails ->
if (showDetails) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Row {
Column(
modifier = Modifier
.weight(1f)
.padding(16.dp)
) {
Text(
text = app.labelOverride ?: app.label,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.clickable {
try {
not.contentIntent?.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}
.padding(vertical = 4.dp)
) {
Box(
modifier = Modifier
.padding(horizontal = 12.dp)
.clip(CircleShape)
.background(Color(not.color))
.size(32.dp)
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = icon,
contentDescription = null
.sharedBounds(
rememberSharedContentState("label"),
this@AnimatedContent,
),
)
if (!app.isPrivate) {
val tags by viewModel.tags.collectAsState(emptyList())
if (tags.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = 1.dp, bottom = 4.dp),
text = tags.joinToString(separator = " #", prefix = "#"),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.labelSmall
)
}
Column(
modifier = Modifier.weight(1f)
) {
if (not.title != null) {
Text(
not.title!!,
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (not.text != null) {
Text(
not.text!!,
modifier = Modifier.padding(top = 2.dp),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
app.versionName?.let {
Text(
text = stringResource(R.string.app_info_version, it),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = app.componentName.packageName,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 1.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Text(
stringResource(R.string.profile_private_profile_state_locked),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp),
color = MaterialTheme.colorScheme.secondary,
)
}
}
ShapedLauncherIcon(
size = 48.dp,
modifier = Modifier
.padding(16.dp),
badge = { badge },
icon = { icon },
)
}
val notifications by viewModel.notifications.collectAsState(emptyList())
AnimatedVisibility(notifications.isNotEmpty()) {
var showAllNotifications by remember { mutableStateOf(false) }
AnimatedContent(
showAllNotifications || notifications.size == 1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.border(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
MaterialTheme.shapes.small
)
.clip(MaterialTheme.shapes.small)
) { showAll ->
if (showAll) {
Column(
modifier = Modifier.animateContentSize()
) {
for ((i, not) in notifications.withIndex()) {
val icon =
remember(not.smallIcon) {
not.smallIcon?.loadDrawable(
context
)
}
if (not.title == null && not.text == null) continue
if (i > 0) {
HorizontalDivider()
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
try {
not.contentIntent?.sendWithBackgroundPermission(
context
)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}
.padding(vertical = 4.dp)
) {
Box(
modifier = Modifier
.padding(horizontal = 12.dp)
.clip(CircleShape)
.background(Color(not.color))
.size(32.dp)
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = icon,
contentDescription = null
)
}
Column(
modifier = Modifier.weight(1f)
) {
if (not.title != null) {
Text(
not.title!!,
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (not.text != null) {
Text(
not.text!!,
modifier = Modifier.padding(top = 2.dp),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
if (not.isClearable) {
IconButton(
onClick = {
viewModel.clearNotification(not)
}
) {
Icon(Icons.Rounded.Clear, null)
}
}
}
}
}
if (not.isClearable) {
} else {
Row(
modifier = Modifier
.clickable {
showAllNotifications = true
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Rounded.Notifications,
null,
modifier = Modifier.padding(horizontal = 16.dp)
)
Text(
pluralStringResource(
R.plurals.app_info_notifications,
notifications.size,
notifications.size
),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f),
)
Icon(
Icons.AutoMirrored.Rounded.NavigateNext,
null,
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
}
val shortcuts by viewModel.children.collectAsState(emptyList())
if (shortcuts.isNotEmpty()) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.border(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
MaterialTheme.shapes.small
)
.clip(MaterialTheme.shapes.small)
) {
for ((i, shortcut) in shortcuts.withIndex()) {
val isPinned by remember(shortcut) {
viewModel.isChildPinned(
shortcut
)
}.collectAsState(
false
)
val iconSizePx = 32.dp.toPixels()
val icon by
remember {
viewModel.getChildIcon(
shortcut,
iconSizePx.toInt()
)
}.collectAsState(null)
if (i > 0) {
HorizontalDivider()
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
viewModel.launchChild(context, shortcut)
}
.padding(vertical = 4.dp)
) {
ShapedLauncherIcon(
size = 32.dp,
icon = { icon },
shape = CircleShape,
modifier = Modifier
.padding(horizontal = 12.dp)
.size(32.dp),
)
Text(
shortcut.labelOverride ?: shortcut.label,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
IconButton(
onClick = {
viewModel.clearNotification(not)
if (isPinned) {
viewModel.unpinChild(shortcut)
} else {
viewModel.pinChild(shortcut)
}
}
) {
Icon(Icons.Rounded.Clear, null)
Icon(
if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline,
stringResource(if (isPinned) R.string.menu_favorites_unpin else R.string.menu_favorites_pin),
)
}
}
}
}
}
} else {
Row(
modifier = Modifier
.clickable {
showAllNotifications = true
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Rounded.Notifications,
null,
modifier = Modifier.padding(horizontal = 16.dp)
)
Text(
pluralStringResource(
R.plurals.app_info_notifications,
notifications.size,
notifications.size
),
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.weight(1f),
)
Icon(
Icons.AutoMirrored.Rounded.NavigateNext,
null,
modifier = Modifier.padding(horizontal = 12.dp)
)
}
}
}
}
val shortcuts by viewModel.children.collectAsState(emptyList())
if (shortcuts.isNotEmpty()) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp)
.border(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
MaterialTheme.shapes.small
)
.clip(MaterialTheme.shapes.small)
) {
for ((i, shortcut) in shortcuts.withIndex()) {
val isPinned by remember(shortcut) { viewModel.isChildPinned(shortcut) }.collectAsState(
false
val toolbarActions = mutableListOf<ToolbarAction>()
if (LocalFavoritesEnabled.current) {
val isPinned by viewModel.isPinned.collectAsState(false)
val favAction = if (isPinned) {
DefaultToolbarAction(
label = stringResource(R.string.menu_favorites_unpin),
icon = Icons.Rounded.Star,
action = {
viewModel.unpin()
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_favorites_pin),
icon = Icons.Rounded.StarOutline,
action = {
viewModel.pin()
})
}
toolbarActions.add(favAction)
}
if (!app.isPrivate) {
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_app_info),
icon = Icons.Rounded.Info
) {
app.openAppDetails(context)
})
}
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_launch),
icon = Icons.AutoMirrored.Rounded.OpenInNew,
action = {
viewModel.launch(context)
}
)
)
val iconSizePx = 32.dp.toPixels()
val icon by
remember {
viewModel.getChildIcon(
shortcut,
iconSizePx.toInt()
)
}.collectAsState(null)
if (i > 0) {
HorizontalDivider()
val sheetManager = LocalBottomSheetManager.current
if (!app.isPrivate) {
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_customize),
icon = Icons.Rounded.Tune,
action = { sheetManager.showCustomizeSearchableModal(app) }
))
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable {
viewModel.launchChild(context, shortcut)
}
.padding(vertical = 4.dp)
) {
ShapedLauncherIcon(
size = 32.dp,
icon = { icon },
shape = CircleShape,
modifier = Modifier
.padding(horizontal = 12.dp)
.size(32.dp),
)
Text(
shortcut.labelOverride ?: shortcut.label,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
IconButton(
onClick = {
if (isPinned) {
viewModel.unpinChild(shortcut)
} else {
viewModel.pinChild(shortcut)
if (!app.isPrivate) {
val storeDetails = remember(app) { app.getStoreDetails(context) }
val shareAction = if (storeDetails == null) {
DefaultToolbarAction(
label = stringResource(R.string.menu_share),
icon = Icons.Rounded.Share
) {
scope.launch {
app.shareApkFile(context)
}
}
) {
Icon(
if (isPinned) Icons.Rounded.Star else Icons.Rounded.StarOutline,
stringResource(if (isPinned) R.string.menu_favorites_unpin else R.string.menu_favorites_pin),
} else {
SubmenuToolbarAction(
label = stringResource(R.string.menu_share),
icon = Icons.Rounded.Share,
children = listOf(
DefaultToolbarAction(
label = stringResource(
R.string.menu_share_store_link,
storeDetails.label
),
icon = Icons.Rounded.Link,
action = {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(
Intent.EXTRA_TEXT,
storeDetails.url
)
shareIntent.type = "text/plain"
context.startActivity(
Intent.createChooser(
shareIntent,
null
)
)
}
),
DefaultToolbarAction(
label = stringResource(R.string.menu_share_apk_file),
icon = Icons.Rounded.Android
) {
scope.launch {
app.shareApkFile(context)
}
}
)
)
}
toolbarActions.add(shareAction)
}
}
}
}
val toolbarActions = mutableListOf<ToolbarAction>()
if (LocalFavoritesEnabled.current) {
val isPinned by viewModel.isPinned.collectAsState(false)
val favAction = if (isPinned) {
DefaultToolbarAction(
label = stringResource(R.string.menu_favorites_unpin),
icon = Icons.Rounded.Star,
action = {
viewModel.unpin()
if (app.canUninstall) {
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_uninstall),
icon = Icons.Rounded.Delete,
) {
app.uninstall(context)
onBack()
}
)
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_favorites_pin),
icon = Icons.Rounded.StarOutline,
action = {
viewModel.pin()
})
}
toolbarActions.add(favAction)
}
if (!app.isPrivate) {
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_app_info),
icon = Icons.Rounded.Info
) {
app.openAppDetails(context)
})
}
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_launch),
icon = Icons.AutoMirrored.Rounded.OpenInNew,
action = {
viewModel.launch(context)
}
)
)
val sheetManager = LocalBottomSheetManager.current
if (!app.isPrivate) {
toolbarActions.add(DefaultToolbarAction(
label = stringResource(R.string.menu_customize),
icon = Icons.Rounded.Tune,
action = { sheetManager.showCustomizeSearchableModal(app) }
))
}
if (!app.isPrivate) {
val storeDetails = remember(app) { app.getStoreDetails(context) }
val shareAction = if (storeDetails == null) {
DefaultToolbarAction(
label = stringResource(R.string.menu_share),
icon = Icons.Rounded.Share
) {
scope.launch {
app.shareApkFile(context)
}
}
} else {
SubmenuToolbarAction(
label = stringResource(R.string.menu_share),
icon = Icons.Rounded.Share,
children = listOf(
DefaultToolbarAction(
label = stringResource(
R.string.menu_share_store_link,
storeDetails.label
),
icon = Icons.Rounded.Link,
action = {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_TEXT, storeDetails.url)
shareIntent.type = "text/plain"
context.startActivity(Intent.createChooser(shareIntent, null))
Toolbar(
leftActions = listOf(
DefaultToolbarAction(
label = stringResource(id = R.string.menu_back),
icon = Icons.AutoMirrored.Rounded.ArrowBack
) {
onBack()
}
),
DefaultToolbarAction(
label = stringResource(R.string.menu_share_apk_file),
icon = Icons.Rounded.Android
) {
scope.launch {
app.shareApkFile(context)
}
}
rightActions = toolbarActions
)
)
}
} else {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (LocalGridSettings.current.showListIcons) {
ShapedLauncherIcon(
size = LocalGridSettings.current.iconSize.dp,
modifier = Modifier
.padding(end = 16.dp),
badge = { badge },
icon = { icon },
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = app.labelOverride ?: app.label,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.sharedBounds(
rememberSharedContentState("label"),
this@AnimatedContent,
),
)
}
}
toolbarActions.add(shareAction)
}
if (app.canUninstall) {
toolbarActions.add(
DefaultToolbarAction(
label = stringResource(R.string.menu_uninstall),
icon = Icons.Rounded.Delete,
) {
app.uninstall(context)
onBack()
}
)
}
Toolbar(
leftActions = listOf(
DefaultToolbarAction(
label = stringResource(id = R.string.menu_back),
icon = Icons.AutoMirrored.Rounded.ArrowBack
) {
onBack()
}
),
rightActions = toolbarActions
)
}
}
@@ -507,6 +565,7 @@ fun AppItemGridPopup(
y = lerp(-16.dp, 0.dp, animationProgress)
),
app = app,
showDetails = true,
onBack = onDismiss
)
}
@@ -4,7 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -22,9 +22,9 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -36,6 +36,8 @@ import de.mm20.launcher2.search.Application
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.launcher.search.common.grid.GridItem
import de.mm20.launcher2.ui.launcher.search.common.grid.GridResults
import de.mm20.launcher2.ui.launcher.search.common.list.ListItem
import de.mm20.launcher2.ui.launcher.search.common.list.ListResults
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalGridSettings
@@ -47,158 +49,192 @@ fun LazyListScope.AppResults(
isProfileLocked: Boolean = false,
onProfileLockChange: ((Profile, Boolean) -> Unit)? = null,
apps: List<Application>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
highlightedItem: Application? = null,
columns: Int,
reverse: Boolean,
showList: Boolean,
) {
GridResults(
key = "apps",
items = apps.filter { it.user == profiles[selectedProfileIndex].userHandle },
before = if (profiles.size > 1) {
{
Column(
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top,
val before = if (profiles.size > 1) {
@Composable {
Column(
verticalArrangement = if (reverse) Arrangement.BottomReversed else Arrangement.Top,
) {
PrimaryScrollableTabRow(
selectedTabIndex = selectedProfileIndex,
containerColor = Color.Transparent,
edgePadding = 16.dp,
divider = {}
) {
PrimaryScrollableTabRow(
selectedTabIndex = selectedProfileIndex,
containerColor = Color.Transparent,
edgePadding = 16.dp,
divider = {}
) {
for ((i, profile) in profiles.withIndex()) {
LeadingIconTab(
selected = selectedProfileIndex == profiles.indexOf(profile),
text = {
Text(
when (profile.type) {
Profile.Type.Personal -> stringResource(R.string.apps_profile_main)
Profile.Type.Work -> stringResource(R.string.apps_profile_work)
Profile.Type.Private -> stringResource(R.string.apps_profile_private)
}
)
},
icon = {
for ((i, profile) in profiles.withIndex()) {
LeadingIconTab(
selected = selectedProfileIndex == profiles.indexOf(profile),
text = {
Text(
when (profile.type) {
Profile.Type.Personal -> Icon(
Icons.Rounded.Person,
contentDescription = null
)
Profile.Type.Work -> Icon(
Icons.Rounded.Work,
contentDescription = null
)
Profile.Type.Private -> Icon(
Icons.Rounded.PrivateSpace,
contentDescription = null
)
Profile.Type.Personal -> stringResource(R.string.apps_profile_main)
Profile.Type.Work -> stringResource(R.string.apps_profile_work)
Profile.Type.Private -> stringResource(R.string.apps_profile_private)
}
},
onClick = {
onProfileSelected(i)
)
},
icon = {
when (profile.type) {
Profile.Type.Personal -> Icon(
Icons.Rounded.Person,
contentDescription = null
)
Profile.Type.Work -> Icon(
Icons.Rounded.Work,
contentDescription = null
)
Profile.Type.Private -> Icon(
Icons.Rounded.PrivateSpace,
contentDescription = null
)
}
)
}
},
onClick = {
onProfileSelected(i)
}
)
}
HorizontalDivider()
}
val profileType = profiles[selectedProfileIndex].type
if (profileType != Profile.Type.Personal) {
if (isProfileLocked) {
Column(
modifier = Modifier
.padding(12.dp)
.fillMaxWidth()
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, MaterialTheme.shapes.small)
.background(MaterialTheme.colorScheme.surfaceContainer, MaterialTheme.shapes.small)
.padding(vertical = 64.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.secondary,
if (!showList || isProfileLocked) {
HorizontalDivider()
}
val profileType = profiles[selectedProfileIndex].type
if (profileType != Profile.Type.Personal) {
if (isProfileLocked) {
Column(
modifier = Modifier
.padding(12.dp)
.fillMaxWidth()
.border(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
MaterialTheme.shapes.small
)
Text(
stringResource(
if (profileType == Profile.Type.Work) R.string.profile_work_profile_state_locked
else R.string.profile_private_profile_state_locked
),
modifier = Modifier.padding(top = 8.dp),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall,
.background(
MaterialTheme.colorScheme.surfaceContainer,
MaterialTheme.shapes.small
)
if (showProfileLockControls) {
Button(
modifier = Modifier.padding(top = 32.dp),
onClick = {
onProfileLockChange?.invoke(
profiles[selectedProfileIndex],
false
)
},
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
) {
Icon(
if (profileType == Profile.Type.Work) Icons.Rounded.Work else Icons.Rounded.LockOpen,
contentDescription = null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
.padding(vertical = 64.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.secondary,
)
Text(
stringResource(
if (profileType == Profile.Type.Work) R.string.profile_work_profile_state_locked
else R.string.profile_private_profile_state_locked
),
modifier = Modifier.padding(top = 8.dp),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall,
)
if (showProfileLockControls) {
Button(
modifier = Modifier.padding(top = 32.dp),
onClick = {
onProfileLockChange?.invoke(
profiles[selectedProfileIndex],
false
)
Text(
stringResource(
if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_unlock
else R.string.profile_private_profile_action_unlock
)
},
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
) {
Icon(
if (profileType == Profile.Type.Work) Icons.Rounded.Work else Icons.Rounded.LockOpen,
contentDescription = null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(
stringResource(
if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_unlock
else R.string.profile_private_profile_action_unlock
)
}
)
}
}
} else if (showProfileLockControls) {
FilledTonalButton(
}
} else if (showProfileLockControls) {
FilledTonalButton(
modifier = Modifier
.padding(12.dp)
.fillMaxWidth(),
onClick = {
onProfileLockChange?.invoke(
profiles[selectedProfileIndex],
true
)
},
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
) {
Icon(
if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock,
contentDescription = null,
modifier = Modifier
.padding(12.dp)
.fillMaxWidth(),
onClick = {
onProfileLockChange?.invoke(
profiles[selectedProfileIndex],
true
)
},
contentPadding = ButtonDefaults.TextButtonWithIconContentPadding,
) {
Icon(
if (profileType == Profile.Type.Work) Icons.Rounded.WorkOff else Icons.Rounded.Lock,
contentDescription = null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(
stringResource(
if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_lock
else R.string.profile_private_profile_action_lock
)
Text(
stringResource(
if (profileType == Profile.Type.Work) R.string.profile_work_profile_action_lock
else R.string.profile_private_profile_action_lock
)
)
}
)
}
}
}
}
} else null,
itemContent = {
GridItem(
item = it,
showLabels = LocalGridSettings.current.showLabels,
highlight = it.key == highlightedItem?.key
)
},
reverse = reverse,
columns = columns,
)
}
} else null
if (showList) {
ListResults(
key = "apps",
items = apps.filter { it.user == profiles[selectedProfileIndex].userHandle },
before = before?.let { { it() } },
selectedIndex = selectedIndex,
itemContent = { app, showDetails, index ->
ListItem(
modifier = Modifier
.fillMaxWidth(),
item = app,
showDetails = showDetails,
onShowDetails = { onSelect(if(it) index else -1) },
highlight = highlightedItem?.key == app.key
)
},
reverse = reverse,
)
} else {
GridResults(
key = "apps",
items = apps.filter { it.user == profiles[selectedProfileIndex].userHandle },
before = before,
itemContent = {
GridItem(
item = it,
showLabels = LocalGridSettings.current.showLabels,
highlight = it.key == highlightedItem?.key
)
},
reverse = reverse,
columns = columns,
)
}
}
@@ -16,6 +16,7 @@ import de.mm20.launcher2.notifications.Notification
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.search.ContactSearchSettings
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
@@ -53,6 +54,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
private val appShortcutRepository: AppShortcutRepository by inject()
private val permissionsManager: PermissionsManager by inject()
private val locationSearchSettings: LocationSearchSettings by inject()
private val contactSearchSettings: ContactSearchSettings by inject()
val isUpToDate = MutableStateFlow(true)
@@ -138,7 +140,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
}
val bundle = options.toBundle()
if (searchable.launch(context, bundle)) {
favoritesService.reportLaunch(searchable)
reportUsage(searchable)
return true
} else if (searchable is Application || searchable is AppShortcut) {
favoritesService.reset(searchable)
@@ -168,7 +170,7 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
fun launchChild(context: Context, child: SavableSearchable) {
if (child.launch(context, null)) {
favoritesService.reportLaunch(child)
reportUsage(child)
}
}
@@ -246,4 +248,11 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
val mapTileServerUrl = locationSearchSettings.tileServer
.map { it ?: LocationSearchSettings.DefaultTileServerUrl }
.stateIn(viewModelScope, SharingStarted.Lazily, "")
val callOnTap = contactSearchSettings.callOnTap
.stateIn(viewModelScope, SharingStarted.Lazily, false)
fun reportUsage(searchable: SavableSearchable) {
favoritesService.reportLaunch(searchable)
}
}
@@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.launcher.search.common.grid
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState
@@ -13,6 +12,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
@@ -110,6 +110,7 @@ fun GridItem(
Column(
modifier = modifier
.padding(4.dp)
.combinedClickable(
onClick = {
if (!launchOnPress || !viewModel.launch(context, bounds)) {
@@ -170,7 +171,9 @@ fun GridItem(
modifier = Modifier
.padding(4.dp)
.onGloballyPositioned {
bounds = it.boundsInWindow().roundToIntRect()
bounds = it
.boundsInWindow()
.roundToIntRect()
} then
if (highlight) Modifier.background(
MaterialTheme.colorScheme.surface,
@@ -195,10 +198,10 @@ fun GridItem(
color = MaterialTheme.colorScheme.onBackground,
)
}
}
if (showPopup) {
ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false })
}
if (showPopup) {
ItemPopup(origin = bounds, searchable = item, onDismissRequest = { showPopup = false })
}
}
@@ -398,7 +401,7 @@ private fun Modifier.placeOverlay(
constraints.maxHeight - placeable.height,
),
animationProgress.pow(2)
).toInt()
)
)
}
}
@@ -410,4 +413,4 @@ private fun lerp(start: Float, stop: Float, fraction: Float): Float {
private fun lerp(start: Int, stop: Int, fraction: Float): Int {
return start + (fraction * (stop - start)).toInt()
}
}
@@ -83,8 +83,8 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
.padding(
top = if (it == 0) 8.dp else 0.dp,
bottom = if (it == rows - 1) 8.dp else 0.dp,
start = 4.dp,
end = 4.dp,
start = if (columns == 1) 0.dp else 4.dp,
end = if (columns == 1) 0.dp else 4.dp,
)
) {
Row {
@@ -94,7 +94,6 @@ fun <T : SavableSearchable> LazyListScope.GridResults(
Box(
modifier = Modifier
.weight(1f)
.padding(4.dp)
) {
itemContent(item)
}
@@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
@@ -22,6 +23,7 @@ import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntRect
import de.mm20.launcher2.search.AppShortcut
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.Article
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.Contact
@@ -30,6 +32,7 @@ import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Website
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.launcher.search.apps.AppItem
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import de.mm20.launcher2.ui.launcher.search.contacts.ContactItem
@@ -80,6 +83,25 @@ fun ListItem(
LocalContentColor provides MaterialTheme.colorScheme.onSurface
) {
when (item) {
is Application -> {
AppItem(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 9999.dp) // we have infinite space, but there is an inner scroll that needs a constraint
.combinedClickable(
enabled = !showDetails,
onClick = {
if (!viewModel.launch(context, bounds)) {
onShowDetails(true)
}
},
onLongClick = { onShowDetails(true) }
),
app = item,
showDetails = showDetails,
onBack = { onShowDetails(false) }
)
}
is Contact -> {
ContactItem(
modifier = Modifier
@@ -83,6 +83,8 @@ import de.mm20.launcher2.ui.modifier.scale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import androidx.core.net.toUri
import de.mm20.launcher2.ktx.checkPermission
@Composable
fun ContactItem(
@@ -101,6 +103,7 @@ fun ContactItem(
}
val icon by viewModel.icon.collectAsStateWithLifecycle()
val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(false)
val badge by viewModel.badge.collectAsState(null)
SharedTransitionLayout {
@@ -163,6 +166,7 @@ fun ContactItem(
.fillMaxWidth(),
secondaryAction = {
IconButton(onClick = {
viewModel.reportUsage(contact)
context.tryStartActivity(
Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("smsto:${it.number}")
@@ -180,10 +184,14 @@ fun ContactItem(
expandedSection = if (it) 0 else -1
},
onContact = {
viewModel.reportUsage(contact)
context.tryStartActivity(
Intent(Intent.ACTION_DIAL).apply {
data = Uri.parse("tel:${it.number}")
}
Intent(
if (callOnTap)
Intent.ACTION_CALL
else
Intent.ACTION_DIAL
).setData("tel:${it.number}".toUri())
)
},
copyText = { it.number },
@@ -208,6 +216,7 @@ fun ContactItem(
expandedSection = if (it) 1 else -1
},
onContact = {
viewModel.reportUsage(contact)
context.tryStartActivity(
Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:${it.address}")
@@ -231,6 +240,7 @@ fun ContactItem(
secondaryAction = if (canNavigate) {
{
IconButton(onClick = {
viewModel.reportUsage(contact)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).apply {
data =
@@ -254,6 +264,7 @@ fun ContactItem(
expandedSection = if (it) 2 else -1
},
onContact = {
viewModel.reportUsage(contact)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("geo:0,0?q=${it.address}")
@@ -295,11 +306,22 @@ fun ContactItem(
app.key
}
}
val itemsWithPermission = remember(app) {
app.value.filter {
// exclude activities we have no permission for
val resolvedActivityInfo = context.packageManager.resolveActivity(
Intent(Intent.ACTION_VIEW).setDataAndType(it.uri, it.mimeType),
0
)?.activityInfo ?: return@filter false
resolvedActivityInfo.permission == null || context.checkPermission(resolvedActivityInfo.permission)
}
}
ContactInfo(
icon = Icons.AutoMirrored.Rounded.OpenInNew,
customIcon = appIcon,
label = label,
items = app.value,
items = itemsWithPermission,
itemLabel = { it.label },
expanded = expandedSection == 3 + i,
modifier = Modifier
@@ -309,6 +331,7 @@ fun ContactItem(
expandedSection = if (it) 3 + i else -1
},
onContact = {
viewModel.reportUsage(contact)
context.tryStartActivity(
Intent(Intent.ACTION_VIEW).apply {
setDataAndType(
File diff suppressed because it is too large Load Diff
@@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -38,7 +37,9 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -48,6 +49,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
@@ -75,6 +77,7 @@ import de.mm20.launcher2.ui.ktx.contrast
import de.mm20.launcher2.ui.ktx.hue
import de.mm20.launcher2.ui.ktx.hueRotate
import de.mm20.launcher2.ui.ktx.invert
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import org.koin.android.ext.koin.androidContext
import org.koin.core.component.KoinComponent
@@ -166,18 +169,19 @@ fun MapTiles(
fadeOut() + scaleOut(targetScale = scale)
}
) { (start, stop, zoom) ->
val sideLength = stop.x - start.x + 1
Column(modifier = Modifier.fillMaxWidth()) {
var tileWidth by remember { mutableIntStateOf(0) }
Column(modifier = Modifier
.fillMaxWidth()
// Needed to force all tiles to be the _exact_ same size. With weight(1f) we get rounding errors and gaps.
.onSizeChanged { tileWidth = it.width / (stop.x - start.x + 1) }
) {
for (y in start.y..stop.y) {
Row(
modifier = Modifier
.fillMaxWidth()
) {
Row(modifier = Modifier.fillMaxWidth()) {
for (x in start.x..stop.x) {
AsyncImage(
modifier = Modifier
.weight(1f / sideLength)
.aspectRatio(1f)
.width(tileWidth.toDp())
.height(tileWidth.toDp())
.background(MaterialTheme.colorScheme.secondaryContainer),
imageLoader = MapTileLoader.loader,
model = MapTileLoader.getTileRequest(tileServerUrl, x, y, zoom),
@@ -31,6 +31,7 @@ import androidx.compose.material.icons.rounded.Height
import androidx.compose.material.icons.rounded.HorizontalSplit
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.Tune
import androidx.compose.material.icons.rounded.VerticalSplit
@@ -67,6 +68,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.ClockWidgetAlignment
import de.mm20.launcher2.preferences.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.LocalTime
@@ -83,6 +85,7 @@ import de.mm20.launcher2.ui.launcher.widgets.clock.clocks.SegmentClock
import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider
import de.mm20.launcher2.ui.locals.LocalPreferDarkContentOverWallpaper
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreenVM
import de.mm20.launcher2.ui.utils.isTwentyFourHours
import org.koin.androidx.compose.inject
@Composable
@@ -164,7 +167,8 @@ fun ClockWidget(
Box(
modifier = Modifier
.then(if (fillScreenHeight) Modifier.weight(1f) else Modifier)
.fillMaxWidth().padding(horizontal = if (compact == true) 0.dp else 24.dp),
.fillMaxWidth()
.padding(horizontal = if (compact == true) 0.dp else 24.dp),
contentAlignment = when (alignment) {
ClockWidgetAlignment.Center -> Alignment.Center
ClockWidgetAlignment.Top -> Alignment.TopCenter
@@ -265,34 +269,42 @@ fun Clock(
darkColors: Boolean = false
) {
val time = LocalTime.current
val context = LocalContext.current
val clockSettings: ClockWidgetSettings by inject()
val showSeconds by clockSettings.showSeconds.collectAsState(initial = false)
val useThemeColor by clockSettings.useThemeColor.collectAsState(initial = false)
val timeFormat by clockSettings.timeFormat.collectAsState(null)
if (timeFormat == null) return
val isTwentyFourHours = timeFormat!!.isTwentyFourHours(context)
when (style) {
is ClockWidgetStyle.Digital1 -> DigitalClock1(
time,
style,
compact,
showSeconds,
useThemeColor,
darkColors
time = time,
compact = compact,
showSeconds = showSeconds,
twentyFourHours = isTwentyFourHours,
useThemeColor = useThemeColor,
darkColors = darkColors,
)
is ClockWidgetStyle.Digital2 -> DigitalClock2(
time,
compact,
showSeconds,
useThemeColor,
darkColors
time = time,
compact = compact,
showSeconds = showSeconds,
twentyFourHours = isTwentyFourHours,
useThemeColor = useThemeColor,
darkColors = darkColors,
)
is ClockWidgetStyle.Binary -> BinaryClock(
time,
compact,
showSeconds,
useThemeColor,
darkColors
time = time,
compact = compact,
showSeconds = showSeconds,
twentyFourHours = isTwentyFourHours,
useThemeColor = useThemeColor,
darkColors = darkColors,
)
is ClockWidgetStyle.Analog -> AnalogClock(
@@ -304,19 +316,21 @@ fun Clock(
)
is ClockWidgetStyle.Orbit -> OrbitClock(
time,
compact,
showSeconds,
useThemeColor,
darkColors
time = time,
compact = compact,
showSeconds = showSeconds,
twentyFourHours = isTwentyFourHours,
useThemeColor = useThemeColor,
darkColors = darkColors,
)
is ClockWidgetStyle.Segment -> SegmentClock(
time,
compact,
showSeconds,
useThemeColor,
darkColors
time = time,
compact = compact,
showSeconds = showSeconds,
twentyFourHours = isTwentyFourHours,
useThemeColor = useThemeColor,
darkColors = darkColors,
)
is ClockWidgetStyle.Custom -> CustomClock(style, compact, useThemeColor, darkColors)
@@ -349,6 +363,7 @@ fun ConfigureClockWidgetSheet(
val fillHeight by viewModel.fillHeight.collectAsState()
val alignment by viewModel.alignment.collectAsState()
val showSeconds by viewModel.showSeconds.collectAsState()
val timeFormat by viewModel.timeFormat.collectAsState()
val useAccentColor by viewModel.useThemeColor.collectAsState()
val parts by viewModel.parts.collectAsState()
@@ -488,13 +503,63 @@ fun ConfigureClockWidgetSheet(
AnimatedVisibility(compact == false && style !is ClockWidgetStyle.Custom) {
SwitchPreference(
title = stringResource(R.string.preference_clock_widget_show_seconds),
icon = Icons.Rounded.AccessTime,
icon = Icons.Rounded.Timer,
value = showSeconds,
onValueChanged = {
viewModel.setShowSeconds(it)
}
)
}
AnimatedVisibility(
style !is ClockWidgetStyle.Analog &&
style !is ClockWidgetStyle.Custom &&
style !is ClockWidgetStyle.Empty
) {
var showDropdown by remember { mutableStateOf(false) }
Preference(
title = stringResource(R.string.preference_clock_widget_time_format),
summary = when (timeFormat) {
TimeFormat.TwelveHour -> stringResource(R.string.preference_clock_widget_time_format_12h)
TimeFormat.TwentyFourHour -> stringResource(R.string.preference_clock_widget_time_format_24h)
TimeFormat.System -> stringResource(R.string.preference_clock_widget_time_format_system)
},
icon = Icons.Rounded.AccessTime,
onClick = {
showDropdown = true
}
)
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false }) {
DropdownMenuItem(
text = {
Text(stringResource(R.string.preference_clock_widget_time_format_system))
},
onClick = {
viewModel.setTimeFormat(TimeFormat.System)
showDropdown = false
}
)
DropdownMenuItem(
text = {
Text(stringResource(R.string.preference_clock_widget_time_format_24h))
},
onClick = {
viewModel.setTimeFormat(TimeFormat.TwentyFourHour)
showDropdown = false
}
)
DropdownMenuItem(
text = {
Text(stringResource(R.string.preference_clock_widget_time_format_12h))
},
onClick = {
viewModel.setTimeFormat(TimeFormat.TwelveHour)
showDropdown = false
}
)
}
}
}
}
OutlinedCard(
@@ -13,14 +13,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.utils.isTwentyFourHours
import java.util.Calendar
@Composable
fun BinaryClock(
time: Long,
compact: Boolean,
twentyFourHours: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
@@ -30,8 +35,8 @@ fun BinaryClock(
date.timeInMillis = time
val second = date[Calendar.SECOND]
val minute = date[Calendar.MINUTE]
var hour = date[Calendar.HOUR]
if (hour == 0) hour = 12
var hour = date[if(!twentyFourHours) Calendar.HOUR else Calendar.HOUR_OF_DAY]
if (!twentyFourHours && hour == 0) hour = 12
val color = if (useThemeColor) {
if (!darkColors) {
@@ -56,11 +61,11 @@ fun BinaryClock(
Row(
modifier = Modifier.padding(start = 0.dp, top = 24.dp, end = 0.dp, bottom = 6.dp)
) {
for (i in 0 until 10) {
val active = if (i < 4) {
hour and (1 shl (3 - i)) != 0
for (i in 0 until if (twentyFourHours) 11 else 10) {
val active = if (i < if (twentyFourHours) 5 else 4) {
hour and (1 shl ((if (twentyFourHours) 4 else 3) - i)) != 0
} else {
minute and (1 shl (9 - i)) != 0
minute and (1 shl ((if (twentyFourHours) 10 else 9) - i)) != 0
}
Box(
modifier = Modifier
@@ -70,7 +75,7 @@ fun BinaryClock(
if (active) color else disabledColor
)
)
if (i == 3) {
if (i == if (twentyFourHours) 4 else 3) {
Box(Modifier.size(8.dp))
}
}
@@ -98,8 +103,8 @@ fun BinaryClock(
horizontalAlignment = Alignment.End
) {
Row {
for (i in 0 until 4) {
val active = hour and (1 shl (3 - i)) != 0
for (i in 0 until if (twentyFourHours) 5 else 4) {
val active = hour and (1 shl ((if (twentyFourHours) 4 else 3) - i)) != 0
Box(
modifier = Modifier
.padding( 4.dp)
@@ -33,6 +33,7 @@ fun DigitalClock1(
time: Long,
style: ClockWidgetStyle.Digital1 = ClockWidgetStyle.Digital1(),
compact: Boolean,
twentyFourHours: Boolean,
showSeconds: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
@@ -40,10 +41,10 @@ fun DigitalClock1(
val verticalLayout = !compact
val format = SimpleDateFormat(
when {
DateFormat.is24HourFormat(LocalContext.current) && verticalLayout -> {
twentyFourHours && verticalLayout -> {
"HH\nmm"
}
DateFormat.is24HourFormat(LocalContext.current) -> {
twentyFourHours -> {
"HH mm"
}
verticalLayout -> {
@@ -21,6 +21,7 @@ fun DigitalClock2(
time: Long,
compact: Boolean,
showSeconds: Boolean,
twentyFourHours: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
@@ -40,14 +41,14 @@ fun DigitalClock2(
}
val formatString = if (verticalLayout && showSeconds) {
if (DateFormat.is24HourFormat(LocalContext.current)) {
if (twentyFourHours) {
"HH:mm:ss"
}
else {
"hh:mm:ss"
}
} else {
if (DateFormat.is24HourFormat(LocalContext.current)) {
if (twentyFourHours) {
"HH:mm"
}
else {
@@ -48,6 +48,7 @@ fun OrbitClock(
time: Long,
compact: Boolean,
showSeconds: Boolean,
twentyFourHours: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
@@ -59,7 +60,7 @@ fun OrbitClock(
val minute = parsed.minute
val hour = parsed.hour
val formattedHour = (
if (DateFormat.is24HourFormat(LocalContext.current))
if (twentyFourHours)
hour
else {
((hour + 11) % 12) + 1
@@ -52,11 +52,12 @@ fun SegmentClock(
time: Long,
compact: Boolean,
showSeconds: Boolean,
twentyFourHours: Boolean,
useThemeColor: Boolean,
darkColors: Boolean,
) {
val parsed = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault())
val hour = if (DateFormat.is24HourFormat(LocalContext.current)) parsed.hour else (((parsed.hour + 11) % 12) + 1)
val hour = if (twentyFourHours) parsed.hour else (((parsed.hour + 11) % 12) + 1)
val minute = parsed.minute
val second = parsed.second
@@ -38,10 +38,12 @@ import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarProviderSettingsScreen
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
import de.mm20.launcher2.ui.settings.contacts.ContactsSettingsScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
@@ -91,16 +93,18 @@ class SettingsActivity : BaseActivity() {
val navController = rememberNavController()
LaunchedEffect(route) {
try {
navController.navigate(route ?: "settings") {
popUpTo("settings") {
inclusive = true
if (route != null) {
try {
navController.navigate(route ?: "settings") {
popUpTo("settings") {
inclusive = true
}
}
}
} catch (e: IllegalArgumentException) {
navController.navigate("settings") {
popUpTo("settings") {
inclusive = true
} catch (e: IllegalArgumentException) {
navController.navigate("settings") {
popUpTo("settings") {
inclusive = true
}
}
}
}
@@ -198,6 +202,11 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/calendar") {
CalendarSearchSettingsScreen()
}
composable("settings/search/calendar/{providerId}") {
CalendarProviderSettingsScreen(
it.arguments?.getString("providerId") ?: return@composable
)
}
composable("settings/search/searchactions") {
SearchActionsSettingsScreen()
}
@@ -219,6 +228,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/favorites") {
FavoritesSettingsScreen()
}
composable("settings/search/contacts") {
ContactsSettingsScreen()
}
composable("settings/integrations") {
IntegrationsSettingsScreen()
}
@@ -0,0 +1,97 @@
package de.mm20.launcher2.ui.settings.calendarsearch
import android.app.PendingIntent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.calendar.providers.CalendarList
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalDarkTheme
@Composable
fun CalendarProviderSettingsScreen(providerId: String) {
val viewModel = viewModel<CalendarProviderSettingsScreenVM>()
LaunchedEffect(providerId) {
viewModel.init(providerId)
}
val enabled by viewModel.isProviderEnabled.collectAsStateWithLifecycle(false)
val calendarLists by viewModel.calendarLists.collectAsStateWithLifecycle(sortedMapOf<String, List<CalendarList>>())
val excludedCalendars by viewModel.excludedCalendars.collectAsStateWithLifecycle(emptySet())
val pluginState by viewModel.pluginState.collectAsStateWithLifecycle(null)
val providerAvailable = providerId == "local" || pluginState != null
PreferenceScreen(
title = pluginState?.plugin?.label ?: stringResource(R.string.preference_search_calendar)
) {
if (!providerAvailable) {
return@PreferenceScreen
}
item {
PreferenceCategory {
SwitchPreference(
title =
if (providerId == "local") stringResource(R.string.preference_search_calendar)
else pluginState?.plugin?.label ?: "",
summary =
if (providerId == "local") stringResource(R.string.preference_search_local_calendar_summary)
else (pluginState?.state as? PluginState.Ready)?.text
?: pluginState?.plugin?.description,
value = enabled && (pluginState == null || pluginState?.state is PluginState.Ready),
onValueChanged = { viewModel.setProviderEnabled(providerId, it) }
)
}
}
items(calendarLists.toList()) { (k, v) ->
PreferenceCategory(
title = k,
) {
for (list in v) {
CheckboxPreference(
title = list.name,
value = !excludedCalendars.contains(list.id),
onValueChanged = { viewModel.setCalendarExcluded(list.id, !it) },
checkboxColors = CheckboxDefaults.colors(
checkedColor = if (list.color == 0) MaterialTheme.colorScheme.primary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 80 else 40)
),
checkmarkColor = if (list.color == 0) MaterialTheme.colorScheme.onPrimary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
),
enabled = enabled,
)
}
}
}
}
}
@@ -0,0 +1,42 @@
package de.mm20.launcher2.ui.settings.calendarsearch
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class CalendarProviderSettingsScreenVM: ViewModel(), KoinComponent {
private val providerId = MutableStateFlow<String>("")
fun init(providerId: String) {
this.providerId.value = providerId
}
private val calendarSearchSettings: CalendarSearchSettings by inject()
private val calendarRepository: CalendarRepository by inject()
private val pluginService: PluginService by inject()
val pluginState = providerId.flatMapLatest { pluginService.getPluginWithState(it) }
val isProviderEnabled = providerId.flatMapLatest { calendarSearchSettings.isProviderEnabled(it) }
fun setProviderEnabled(providerId: String, enabled: Boolean) {
calendarSearchSettings.setProviderEnabled(providerId, enabled)
}
val calendarLists = providerId
.flatMapLatest { calendarRepository.getCalendars(it) }
.map { it.groupBy { it.owner }.toSortedMap(compareBy { it }) }
val excludedCalendars = calendarSearchSettings.excludedCalendars
fun setCalendarExcluded(calendarId: String, excluded: Boolean) {
calendarSearchSettings.setCalendarExcluded(calendarId, excluded)
}
}
@@ -1,32 +1,21 @@
package de.mm20.launcher2.ui.settings.calendarsearch
import android.app.PendingIntent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CalendarToday
import androidx.compose.material.icons.rounded.Checklist
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
@@ -35,163 +24,83 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
import de.mm20.launcher2.plugin.PluginState
import de.mm20.launcher2.search.calendar.CalendarListType
import de.mm20.launcher2.themes.atTone
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.Banner
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.CheckboxPreference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun CalendarSearchSettingsScreen() {
val viewModel: CalendarSearchSettingsScreenVM = viewModel()
val context = LocalContext.current
val navController = LocalNavController.current
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsState(null)
val plugins by viewModel.availablePlugins.collectAsState(emptyList())
val plugins by viewModel.availablePlugins.collectAsStateWithLifecycle(emptyList(), minActiveState = Lifecycle.State.RESUMED)
val enabledProviders by viewModel.enabledProviders.collectAsState(emptySet())
val calendarLists by viewModel.calendarLists.collectAsStateWithLifecycle(
null,
minActiveState = Lifecycle.State.RESUMED
)
val excludedCalendars by viewModel.excludedCalendars.collectAsState(emptyList())
var showDialogForProvider by remember { mutableStateOf<String?>(null) }
PreferenceScreen(title = stringResource(R.string.preference_search_calendar)) {
item {
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search_settings),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
val selectedCalendars = remember(excludedCalendars, calendarLists) {
calendarLists?.count { it.providerId == "local" }
?.minus(excludedCalendars.count {
it.startsWith("local:")
})
}
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_calendar),
summary = if (selectedCalendars != null && calendarLists != null) "$selectedCalendars lists selected"
else stringResource(R.string.preference_search_calendar_summary),
switchValue = enabledProviders.contains("local") && hasCalendarPermission == true,
onSwitchChanged = {
viewModel.setProviderEnabled("local", it)
},
enabled = hasCalendarPermission == true,
onClick = {
showDialogForProvider = "local"
}
)
for (plugin in plugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message
?: stringResource(id = R.string.plugin_state_setup_required),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(id = R.string.plugin_action_setup))
}
}
PreferenceCategory {
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search_settings),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
val selectedCalendars = remember(excludedCalendars, calendarLists) {
calendarLists?.count { it.providerId == plugin.plugin.authority }
?.minus(excludedCalendars.count {
it.startsWith(
"${plugin.plugin.authority}:"
)
})
}
PreferenceWithSwitch(
title = plugin.plugin.label,
enabled = 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 = enabledProviders.contains(plugin.plugin.authority) && state is PluginState.Ready,
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(plugin.plugin.authority, it)
viewModel.setProviderEnabled("local", it)
},
enabled = hasCalendarPermission == true,
onClick = {
showDialogForProvider = plugin.plugin.authority
navController?.navigate("settings/search/calendar/local")
}
)
}
}
}
Log.d("MM20", "${calendarLists.toString()}")
val dialogCalendarLists by remember {
derivedStateOf {
if (showDialogForProvider == null) null
else calendarLists?.filter { it.providerId == showDialogForProvider }
}
}
if (showDialogForProvider != null && dialogCalendarLists != null) {
ModalBottomSheet(
onDismissRequest = {
showDialogForProvider = null
},
) {
val groups = remember(dialogCalendarLists) {
dialogCalendarLists!!.groupBy { it.owner }.entries.sortedBy { it.key }
}
LazyColumn {
items(groups) {
PreferenceCategory(
title = it.key,
iconPadding = false,
) {
for (list in it.value) {
CheckboxPreference(
title = list.name,
iconPadding = false,
value = list.id !in excludedCalendars,
onValueChanged = { value ->
viewModel.setCalendarExcluded(list.id, !value)
},
checkboxColors = CheckboxDefaults.colors(
checkedColor = if (list.color == 0) MaterialTheme.colorScheme.primary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 80 else 40)
),
checkmarkColor = if (list.color == 0) MaterialTheme.colorScheme.onPrimary
else Color(
list.color.atTone(if (LocalDarkTheme.current) 20 else 100)
)
)
)
}
for (plugin in plugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {
Banner(
modifier = Modifier.padding(16.dp),
text = state.message
?: stringResource(id = R.string.plugin_state_setup_required),
icon = Icons.Rounded.ErrorOutline,
primaryAction = {
TextButton(onClick = {
try {
state.setupActivity.sendWithBackgroundPermission(context)
} catch (e: PendingIntent.CanceledException) {
CrashReporter.logException(e)
}
}) {
Text(stringResource(id = R.string.plugin_action_setup))
}
}
)
}
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}")
}
)
}
}
}
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.ClockWidgetAlignment
import de.mm20.launcher2.preferences.ClockWidgetColors
import de.mm20.launcher2.preferences.ClockWidgetStyle
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.ui.ClockWidgetSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
@@ -21,7 +22,7 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
settings.setCompact(compact)
}
val availableClockStyles = combine(settings.digital1, settings.custom) {digital1, custom ->
val availableClockStyles = combine(settings.digital1, settings.custom) { digital1, custom ->
listOf(
digital1,
ClockWidgetStyle.Digital2,
@@ -54,6 +55,13 @@ class ClockWidgetSettingsScreenVM : ViewModel(), KoinComponent {
settings.setShowSeconds(showSeconds)
}
val timeFormat = settings.timeFormat
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), TimeFormat.System)
fun setTimeFormat(timeFormat: TimeFormat) {
settings.setTimeFormat(timeFormat)
}
val useThemeColor = settings.useThemeColor
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
@@ -0,0 +1,58 @@
package de.mm20.launcher2.ui.settings.contacts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Call
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.MissingPermissionBanner
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
@Composable
fun ContactsSettingsScreen() {
val viewModel: ContactsSettingsScreenVM = viewModel()
val context = LocalContext.current
val hasCallPermission by viewModel.hasCallPermission.collectAsStateWithLifecycle(null)
val callOnTap by viewModel.callOnTap.collectAsStateWithLifecycle(null)
PreferenceScreen(
title = stringResource(R.string.preference_search_contacts)
) {
item {
PreferenceCategory {
AnimatedVisibility(hasCallPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_call_contacts_settings),
onClick = {
viewModel.requestCallPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
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
)
}
}
}
}
@@ -0,0 +1,30 @@
package de.mm20.launcher2.ui.settings.contacts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.search.ContactSearchSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class ContactsSettingsScreenVM : ViewModel(), KoinComponent {
private val settings: ContactSearchSettings by inject()
private val permissionsManager: PermissionsManager by inject()
val hasCallPermission = permissionsManager.hasPermission(PermissionGroup.Call)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun requestCallPermission(activity: AppCompatActivity) =
permissionsManager.requestPermission(activity, PermissionGroup.Call)
val callOnTap = settings.callOnTap
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCallOnTap(callOnTap: Boolean) =
settings.setCallOnTap(callOnTap)
}
@@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.settings.icons
import android.graphics.drawable.ColorDrawable
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
@@ -17,9 +16,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.FormatPaint
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -42,13 +39,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.preferences.IconShape
import de.mm20.launcher2.preferences.ui.GridSettings
import de.mm20.launcher2.ui.R
@@ -116,6 +110,26 @@ fun IconsSettingsScreen() {
viewModel.setShowLabels(it)
}
)
SwitchPreference(
title = stringResource(R.string.preference_grid_list_style),
summary = stringResource(R.string.preference_grid_list_style_summary),
value = grid.showList,
onValueChanged = {
viewModel.setShowList(it)
}
)
AnimatedVisibility(
grid.showList
) {
SwitchPreference(
title = stringResource(R.string.preference_grid_list_icons),
summary = stringResource(R.string.preference_grid_list_icons_summary),
value = grid.showListIcons,
onValueChanged = {
viewModel.setShowListIcons(it)
}
)
}
SliderPreference(
title = stringResource(R.string.preference_grid_column_count),
value = grid.columnCount,
@@ -430,30 +444,14 @@ fun IconShapePreference(
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val context = LocalContext.current
ShapedLauncherIcon(
size = 48.dp,
icon = {
StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = ContextCompat.getDrawable(
context,
R.mipmap.ic_launcher_foreground
)!!,
scale = 1.5f,
),
backgroundLayer = StaticIconLayer(
icon = ColorDrawable(
context.getColor(R.color.ic_launcher_background)
)
)
)
},
modifier = Modifier.clickable {
onValueChanged(it)
showDialog = false
},
shape = getShape(it)
Box(
modifier = Modifier.clip(getShape(it))
.size(48.dp)
.background(MaterialTheme.colorScheme.primary)
.clickable {
onValueChanged(it)
showDialog = false
}
)
Text(
getShapeName(it) ?: "",
@@ -53,6 +53,14 @@ class IconsSettingsScreenVM(
uiSettings.setGridShowLabels(showLabels)
}
fun setShowList(showList: Boolean) {
uiSettings.setGridShowList(showList)
}
fun setShowListIcons(showIcons: Boolean) {
uiSettings.setGridShowListIcons(showIcons)
}
val iconShape = uiSettings.iconShape
fun setIconShape(iconShape: IconShape) {
uiSettings.setIconShape(iconShape)
@@ -6,7 +6,6 @@ import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.LocationSearchSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.settings.plugins
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
@@ -19,6 +20,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile
@@ -77,7 +79,7 @@ import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun PluginSettingsScreen(pluginId: String) {
val navController = LocalNavController.current
val activity = LocalContext.current as AppCompatActivity
val activity = LocalActivity.current
val context = LocalContext.current
val viewModel: PluginSettingsScreenVM = viewModel()
LaunchedEffect(pluginId) {
@@ -142,7 +144,7 @@ fun PluginSettingsScreen(pluginId: String) {
navigationIcon = {
IconButton(onClick = {
if (navController?.navigateUp() != true) {
activity.onBackPressed()
activity?.onBackPressed()
}
}) {
Icon(
@@ -155,7 +157,7 @@ fun PluginSettingsScreen(pluginId: String) {
if (pluginPackage?.settings != null) {
IconButton(onClick = {
pluginPackage?.settings?.let {
activity.startActivity(it)
activity?.startActivity(it)
}
}) {
Icon(
@@ -322,7 +324,9 @@ fun PluginSettingsScreen(pluginId: String) {
)
}
AnimatedVisibility(pluginPackage?.enabled == true && hasPermission == true) {
Column {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
if (filePlugins.isNotEmpty()) {
PreferenceCategory(
stringResource(R.string.plugin_type_filesearch),
@@ -16,15 +16,13 @@ import androidx.compose.material.icons.rounded.Loop
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Public
import androidx.compose.material.icons.rounded.Sort
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material.icons.rounded.Work
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -33,11 +31,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.Wikipedia
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
@@ -48,7 +46,6 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.PreferenceWithSwitch
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.icons.Wikipedia
import de.mm20.launcher2.ui.launcher.search.filters.SearchFilters
import de.mm20.launcher2.ui.locals.LocalNavController
@@ -57,19 +54,40 @@ fun SearchSettingsScreen() {
val viewModel: SearchSettingsScreenVM = viewModel()
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val navController = LocalNavController.current
var showFilterEditor by remember {
mutableStateOf(false)
}
var showFilterEditor by remember { mutableStateOf(false) }
val plugins by viewModel.plugins.collectAsStateWithLifecycle(emptyList())
val hasCalendarPlugins by remember { derivedStateOf { plugins.any { it.plugin.type == PluginType.Calendar } } }
val hasLocationPlugins by remember { derivedStateOf { plugins.any { it.plugin.type == PluginType.LocationSearch } } }
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(null)
val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(null)
val hasCalendarPermission by viewModel.hasCalendarPermission.collectAsStateWithLifecycle(null)
val hasLocationPermission by viewModel.hasLocationPermission.collectAsStateWithLifecycle(null)
val favorites by viewModel.favorites.collectAsStateWithLifecycle(null)
val appShortcuts by viewModel.appShortcuts.collectAsStateWithLifecycle(null)
val calendar by viewModel.calendarSearch.collectAsStateWithLifecycle(null)
val places by viewModel.placesSearch.collectAsStateWithLifecycle(null)
val contacts by viewModel.contacts.collectAsStateWithLifecycle(null)
val calculator by viewModel.calculator.collectAsStateWithLifecycle(null)
val unitConverter by viewModel.unitConverter.collectAsStateWithLifecycle(null)
val wikipedia by viewModel.wikipedia.collectAsStateWithLifecycle(null)
val websites by viewModel.websites.collectAsStateWithLifecycle(null)
val autoFocus by viewModel.autoFocus.collectAsStateWithLifecycle(null)
val launchOnEnter by viewModel.launchOnEnter.collectAsStateWithLifecycle(null)
val reverseSearchResults by viewModel.reverseSearchResults.collectAsStateWithLifecycle(null)
val filterBar by viewModel.filterBar.collectAsStateWithLifecycle(null)
PreferenceScreen(title = stringResource(R.string.preference_screen_search)) {
item {
PreferenceCategory {
val favorites by viewModel.favorites.collectAsStateWithLifecycle(null)
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_favorites),
summary = stringResource(R.string.preference_search_favorites_summary),
@@ -92,9 +110,6 @@ fun SearchSettingsScreen() {
}
)
val hasContactsPermission by viewModel.hasContactsPermission.collectAsStateWithLifecycle(
null
)
AnimatedVisibility(hasContactsPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_contact_search_settings),
@@ -104,30 +119,53 @@ fun SearchSettingsScreen() {
modifier = Modifier.padding(16.dp)
)
}
val contacts by viewModel.contacts.collectAsStateWithLifecycle(null)
SwitchPreference(
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_contacts),
summary = stringResource(R.string.preference_search_contacts_summary),
icon = Icons.Rounded.Person,
value = contacts == true && hasContactsPermission == true,
onValueChanged = {
switchValue = contacts == true && hasContactsPermission == true,
onSwitchChanged = {
viewModel.setContacts(it)
},
onClick = {
navController?.navigate("settings/search/contacts")
},
enabled = hasContactsPermission == true
)
Preference(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
icon = Icons.Rounded.Today,
onClick = {
navController?.navigate("settings/search/calendar")
},
)
val hasAppShortcutsPermission by viewModel.hasAppShortcutPermission.collectAsStateWithLifecycle(
null
)
if (hasCalendarPlugins) {
Preference(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
icon = Icons.Rounded.Today,
onClick = {
navController?.navigate("settings/search/calendar")
},
)
} else {
AnimatedVisibility(hasCalendarPermission == false) {
MissingPermissionBanner(
text = stringResource(R.string.missing_permission_calendar_search_settings),
onClick = {
viewModel.requestCalendarPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_calendar),
summary = stringResource(R.string.preference_search_calendar_summary),
switchValue = calendar == true,
onSwitchChanged = {
viewModel.setCalendarSearch(it)
},
icon = Icons.Rounded.Today,
enabled = hasCalendarPermission == true,
onClick = {
navController?.navigate("settings/search/calendar/local")
}
)
}
AnimatedVisibility(hasAppShortcutsPermission == false) {
MissingPermissionBanner(
text = stringResource(
@@ -140,7 +178,6 @@ fun SearchSettingsScreen() {
modifier = Modifier.padding(16.dp)
)
}
val appShortcuts by viewModel.appShortcuts.collectAsStateWithLifecycle(null)
SwitchPreference(
title = stringResource(R.string.preference_search_appshortcuts),
summary = stringResource(R.string.preference_search_appshortcuts_summary),
@@ -152,7 +189,6 @@ fun SearchSettingsScreen() {
enabled = hasAppShortcutsPermission == true
)
val calculator by viewModel.calculator.collectAsStateWithLifecycle(null)
SwitchPreference(
title = stringResource(R.string.preference_search_calculator),
summary = stringResource(R.string.preference_search_calculator_summary),
@@ -163,7 +199,6 @@ fun SearchSettingsScreen() {
}
)
val unitConverter by viewModel.unitConverter.collectAsStateWithLifecycle(null)
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_unitconverter),
summary = stringResource(R.string.preference_search_unitconverter_summary),
@@ -177,7 +212,6 @@ fun SearchSettingsScreen() {
}
)
val wikipedia by viewModel.wikipedia.collectAsStateWithLifecycle(null)
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_wikipedia),
summary = stringResource(R.string.preference_search_wikipedia_summary),
@@ -191,7 +225,6 @@ fun SearchSettingsScreen() {
}
)
val websites by viewModel.websites.collectAsStateWithLifecycle(null)
SwitchPreference(
title = stringResource(R.string.preference_search_websites),
summary = stringResource(R.string.preference_search_websites_summary),
@@ -202,14 +235,43 @@ fun SearchSettingsScreen() {
}
)
Preference(
title = stringResource(R.string.preference_search_locations),
summary = stringResource(R.string.preference_search_locations_summary),
icon = Icons.Rounded.Place,
onClick = {
navController?.navigate("settings/search/locations")
}
)
AnimatedVisibility(hasLocationPermission == false) {
MissingPermissionBanner(
text = stringResource(
R.string.missing_permission_location_search,
),
onClick = {
viewModel.requestLocationPermission(context as AppCompatActivity)
},
modifier = Modifier.padding(16.dp)
)
}
if (hasLocationPlugins) {
Preference(
title = stringResource(R.string.preference_search_locations),
summary = stringResource(R.string.preference_search_locations_summary),
icon = Icons.Rounded.Place,
enabled = hasLocationPermission == true,
onClick = {
navController?.navigate("settings/search/locations")
}
)
} else {
PreferenceWithSwitch(
title = stringResource(R.string.preference_search_locations),
summary = stringResource(R.string.preference_search_locations_summary),
icon = Icons.Rounded.Place,
onClick = {
navController?.navigate("settings/search/locations")
},
switchValue = places == true,
onSwitchChanged = {
viewModel.setPlacesSearch(it)
},
enabled = hasLocationPermission == true,
)
}
Preference(
title = stringResource(R.string.preference_screen_search_actions),
@@ -242,7 +304,6 @@ fun SearchSettingsScreen() {
}
}
item {
val filterBar by viewModel.filterBar.collectAsStateWithLifecycle(null)
PreferenceCategory {
Preference(
title = stringResource(R.string.preference_default_filter),
@@ -264,7 +325,7 @@ fun SearchSettingsScreen() {
Preference(
title = stringResource(R.string.preference_customize_filter_bar),
summary = stringResource(R.string.preference_customize_filter_bar_summary),
onClick = {
onClick = {
navController?.navigate("settings/search/filterbar")
}
)
@@ -273,7 +334,6 @@ fun SearchSettingsScreen() {
}
item {
PreferenceCategory {
val autoFocus by viewModel.autoFocus.collectAsStateWithLifecycle(null)
SwitchPreference(
title = stringResource(R.string.preference_search_bar_auto_focus),
summary = stringResource(R.string.preference_search_bar_auto_focus_summary),
@@ -283,7 +343,6 @@ fun SearchSettingsScreen() {
viewModel.setAutoFocus(it)
}
)
val launchOnEnter by viewModel.launchOnEnter.collectAsStateWithLifecycle(null)
SwitchPreference(
title = stringResource(R.string.preference_search_bar_launch_on_enter),
summary = stringResource(R.string.preference_search_bar_launch_on_enter_summary),
@@ -296,9 +355,6 @@ fun SearchSettingsScreen() {
}
item {
PreferenceCategory {
val reverseSearchResults by viewModel.reverseSearchResults.collectAsStateWithLifecycle(
null
)
ListPreference(
title = stringResource(R.string.preference_layout_search_results),
items = listOf(
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.plugins.PluginService
import de.mm20.launcher2.preferences.search.CalculatorSearchSettings
import de.mm20.launcher2.preferences.search.CalendarSearchSettings
import de.mm20.launcher2.preferences.search.ContactSearchSettings
@@ -30,10 +31,11 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
private val websiteSearchSettings: WebsiteSearchSettings by inject()
private val unitConverterSettings: UnitConverterSettings by inject()
private val calculatorSearchSettings: CalculatorSearchSettings by inject()
private val locationSearchSettings: LocationSearchSettings by inject()
private val searchFilterSettings: SearchFilterSettings by inject()
private val pluginService: PluginService by inject()
private val permissionsManager: PermissionsManager by inject()
private val locationSearchSettings: LocationSearchSettings by inject()
val favorites = searchUiSettings.favorites
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@@ -42,6 +44,14 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
searchUiSettings.setFavorites(favorites)
}
val hasCalendarPermission = permissionsManager.hasPermission(PermissionGroup.Calendar)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val calendarSearch = calendarSearchSettings.isProviderEnabled("local")
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCalendarSearch(enabled: Boolean) {
calendarSearchSettings.setProviderEnabled("local", enabled)
}
val hasContactsPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@@ -52,6 +62,22 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
contactSearchSettings.setEnabled(contacts)
}
val hasLocationPermission = permissionsManager.hasPermission(PermissionGroup.Location)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val placesSearch = locationSearchSettings.osmLocations
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setPlacesSearch(enabled: Boolean) {
locationSearchSettings.setOsmLocations(enabled)
}
fun requestLocationPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Location)
}
fun requestCalendarPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Calendar)
}
fun requestContactsPermission(activity: AppCompatActivity) {
permissionsManager.requestPermission(activity, PermissionGroup.Contacts)
}
@@ -130,4 +156,7 @@ class SearchSettingsScreenVM : ViewModel(), KoinComponent {
fun setSearchFilters(searchFilters: SearchFilters) {
searchFilterSettings.setDefaultFilter(searchFilters)
}
val plugins = pluginService.getPluginsWithState(enabled = true)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
}
@@ -0,0 +1,10 @@
package de.mm20.launcher2.ui.utils
import android.content.Context
import android.text.format.DateFormat
import de.mm20.launcher2.preferences.TimeFormat
import de.mm20.launcher2.preferences.TimeFormat.TwentyFourHour
fun TimeFormat.isTwentyFourHours(context: Context): Boolean {
return this == TimeFormat.TwentyFourHour || this == TimeFormat.System && DateFormat.is24HourFormat(context)
}