Merge branch 'main' into Hide-zombie-secure-folder-tab
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+11
-2
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+602
-365
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(
|
||||
|
||||
+14
-9
@@ -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)
|
||||
|
||||
+3
-2
@@ -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 -> {
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+2
-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()
|
||||
}
|
||||
|
||||
+97
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
}
|
||||
+51
-142
@@ -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}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -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)
|
||||
|
||||
-1
@@ -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),
|
||||
|
||||
+106
-50
@@ -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(
|
||||
|
||||
+30
-1
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user