diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index e0da3ee5..275c5bef 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -65,6 +65,9 @@
+
+
+
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPicker.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPicker.kt
new file mode 100644
index 00000000..a664e1c2
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPicker.kt
@@ -0,0 +1,263 @@
+package de.mm20.launcher2.ui.common
+
+import android.content.pm.PackageManager
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowDropDown
+import androidx.compose.material.icons.rounded.FilterAlt
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import de.mm20.launcher2.data.customattrs.CustomIcon
+import de.mm20.launcher2.icons.IconPack
+import de.mm20.launcher2.search.SavableSearchable
+import de.mm20.launcher2.ui.R
+import de.mm20.launcher2.ui.ktx.toPixels
+import de.mm20.launcher2.ui.launcher.sheets.IconPreview
+import de.mm20.launcher2.ui.launcher.sheets.Separator
+import de.mm20.launcher2.ui.locals.LocalGridSettings
+import kotlinx.coroutines.launch
+
+@Composable
+fun IconPicker(
+ searchable: SavableSearchable,
+ onSelect: (CustomIcon?) -> Unit,
+ contentPadding: PaddingValues = PaddingValues(0.dp)
+) {
+ val iconSize = 48.dp
+ val iconSizePx = iconSize.toPixels()
+
+ val context = LocalContext.current
+
+ val scope = rememberCoroutineScope()
+
+ val viewModel: IconPickerVM =
+ remember(searchable.key) { IconPickerVM(searchable) }
+
+ val suggestions by remember { viewModel.getIconSuggestions(iconSizePx.toInt()) }
+ .collectAsState(emptyList())
+
+ val defaultIcon by remember {
+ viewModel.getDefaultIcon(iconSizePx.toInt())
+ }.collectAsState(null)
+
+ var query by remember { mutableStateOf("") }
+ var filterIconPack by remember { mutableStateOf(null) }
+ val isSearching by viewModel.isSearchingIcons
+ val iconResults by viewModel.iconSearchResults
+
+ var showIconPackFilter by remember { mutableStateOf(false) }
+ val installedIconPacks by viewModel.installedIconPacks.collectAsState(null)
+ val noPacksInstalled = installedIconPacks?.isEmpty() == true
+
+ val columns = LocalGridSettings.current.columnCount
+
+ LazyVerticalGrid(
+ modifier = Modifier.fillMaxSize(),
+ columns = GridCells.Fixed(columns),
+ contentPadding = contentPadding,
+ ) {
+
+ item(span = { GridItemSpan(columns) }) {
+ SearchBar(
+ modifier = Modifier.padding(bottom = 16.dp),
+ expanded = false,
+ onExpandedChange = {},
+ inputField = {
+ SearchBarDefaults.InputField(
+ enabled = !noPacksInstalled,
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Rounded.Search,
+ contentDescription = null
+ )
+ },
+ onSearch = {},
+ expanded = false,
+ onExpandedChange = {},
+ placeholder = {
+ Text(
+ stringResource(
+ if (noPacksInstalled) R.string.icon_picker_no_packs_installed else R.string.icon_picker_search_icon
+ )
+ )
+ },
+ query = query,
+ onQueryChange = {
+ query = it
+ scope.launch {
+ viewModel.searchIcon(query, filterIconPack)
+ }
+ },
+ )
+ }
+ ) {
+
+ }
+ }
+
+ if (query.isEmpty()) {
+ if (defaultIcon != null) {
+ item(span = { GridItemSpan(columns) }) {
+ Separator(stringResource(R.string.icon_picker_default_icon))
+ }
+ item {
+ IconPreview(item = defaultIcon, iconSize = iconSize, onClick = {
+ onSelect(null)
+ })
+ }
+ }
+ item(span = { GridItemSpan(columns) }) {
+ Separator(stringResource(R.string.icon_picker_suggestions))
+ }
+
+ if (suggestions.isNotEmpty()) {
+ items(suggestions) {
+ IconPreview(
+ it,
+ iconSize,
+ onClick = { onSelect(it.customIcon) }
+ )
+ }
+ }
+ } else {
+
+ if (!installedIconPacks.isNullOrEmpty()) {
+ item(
+ span = { GridItemSpan(columns) },
+ ) {
+ Button(
+ onClick = { showIconPackFilter = !showIconPackFilter },
+ modifier = Modifier
+ .wrapContentWidth(align = Alignment.CenterHorizontally)
+ .padding(bottom = 16.dp),
+ contentPadding = PaddingValues(
+ horizontal = 16.dp,
+ vertical = 8.dp
+ )
+ ) {
+ if (filterIconPack == null) {
+ Icon(
+ modifier = Modifier
+ .padding(end = ButtonDefaults.IconSpacing)
+ .size(ButtonDefaults.IconSize),
+ imageVector = Icons.Rounded.FilterAlt,
+ contentDescription = null
+ )
+ } else {
+ val icon = remember(filterIconPack?.packageName) {
+ try {
+ filterIconPack?.packageName?.let { pkg ->
+ context.packageManager.getApplicationIcon(pkg)
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ null
+ }
+ }
+ AsyncImage(
+ modifier = Modifier
+ .padding(end = ButtonDefaults.IconSpacing)
+ .size(ButtonDefaults.IconSize),
+ model = icon,
+ contentDescription = null
+ )
+ }
+ DropdownMenu(
+ expanded = showIconPackFilter,
+ onDismissRequest = { showIconPackFilter = false }) {
+ DropdownMenuItem(
+ text = { Text(stringResource(id = R.string.icon_picker_filter_all_packs)) },
+ onClick = {
+ showIconPackFilter = false
+ filterIconPack = null
+ scope.launch {
+ viewModel.searchIcon(query, filterIconPack)
+ }
+ }
+ )
+ installedIconPacks?.forEach { iconPack ->
+ DropdownMenuItem(
+ onClick = {
+ showIconPackFilter = false
+ filterIconPack = iconPack
+ scope.launch {
+ viewModel.searchIcon(query, filterIconPack)
+ }
+ },
+ text = {
+ Text(iconPack.name)
+ })
+ }
+ }
+ Text(
+ text = filterIconPack?.name
+ ?: stringResource(id = R.string.icon_picker_filter_all_packs),
+ modifier = Modifier.animateContentSize()
+ )
+ Icon(
+ Icons.Rounded.ArrowDropDown,
+ modifier = Modifier
+ .padding(start = ButtonDefaults.IconSpacing)
+ .size(ButtonDefaults.IconSize),
+ contentDescription = null
+ )
+ }
+ }
+ }
+
+ items(iconResults) {
+ IconPreview(
+ it,
+ iconSize,
+ onClick = { onSelect(it.customIcon) }
+ )
+ }
+
+ if (isSearching) {
+ item(span = { GridItemSpan(columns) }) {
+ Box(
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(12.dp)
+ .size(24.dp)
+ )
+ }
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPickerVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPickerVM.kt
new file mode 100644
index 00000000..8ad25d37
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPickerVM.kt
@@ -0,0 +1,56 @@
+package de.mm20.launcher2.ui.common
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import de.mm20.launcher2.icons.CustomIconWithPreview
+import de.mm20.launcher2.icons.IconPack
+import de.mm20.launcher2.icons.IconService
+import de.mm20.launcher2.search.SavableSearchable
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import kotlin.coroutines.coroutineContext
+
+class IconPickerVM(
+ private val searchable: SavableSearchable
+): KoinComponent {
+ private val iconService: IconService by inject()
+
+ fun getDefaultIcon(size: Int) = flow {
+ emit(iconService.getUncustomizedDefaultIcon(searchable, size))
+ }
+
+ fun getIconSuggestions(size: Int) = flow {
+ emit(iconService.getCustomIconSuggestions(searchable, size))
+ }
+
+ val installedIconPacks = iconService.getInstalledIconPacks()
+
+ val iconSearchResults = mutableStateOf(emptyList())
+ val isSearchingIcons = mutableStateOf(false)
+
+
+ private var debounceSearchJob: Job? = null
+ suspend fun searchIcon(query: String, iconPack: IconPack?) {
+ debounceSearchJob?.cancelAndJoin()
+ if (query.isBlank()) {
+ iconSearchResults.value = emptyList()
+ isSearchingIcons.value = false
+ return
+ }
+ withContext(coroutineContext) {
+ debounceSearchJob = launch {
+ delay(500)
+ isSearchingIcons.value = true
+ iconSearchResults.value = emptyList()
+ iconSearchResults.value = iconService.searchCustomIcons(query, iconPack)
+ isSearchingIcons.value = false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt
index efa42b24..95d38379 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.FilterChipDefaults
@@ -38,6 +39,7 @@ import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.icons.VectorLayer
import de.mm20.launcher2.search.Tag
+import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.toPixels
import org.koin.androidx.compose.inject
@@ -113,21 +115,31 @@ fun TagChip(
onClick = onClick,
onLongClick = onLongClick
)
- .padding(horizontal = 8.dp),
+ .padding(start = 4.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
val foregroundLayer = (icon as? StaticLauncherIcon)?.foregroundLayer
- AnimatedVisibility(!compact || foregroundLayer is TextLayer) {
+ AnimatedVisibility(!compact || foregroundLayer !is VectorLayer) {
if (foregroundLayer is TextLayer) {
Text(
text = foregroundLayer.text,
- modifier = Modifier.width(FilterChipDefaults.IconSize),
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .width(FilterChipDefaults.IconSize),
textAlign = TextAlign.Center,
)
- } else if (foregroundLayer is VectorLayer && !compact) {
+ } else if (foregroundLayer !is VectorLayer) {
+ ShapedLauncherIcon(
+ modifier = Modifier.padding(start = if(compact) 4.dp else 0.dp),
+ size = InputChipDefaults.AvatarSize,
+ icon = { icon },
+ shape = CircleShape,
+ )
+ } else if (!compact) {
Icon(
modifier = Modifier
+ .padding(start = 4.dp)
.size(FilterChipDefaults.IconSize),
imageVector = foregroundLayer.vector,
contentDescription = null,
@@ -140,7 +152,7 @@ fun TagChip(
tag.tag,
style = MaterialTheme.typography.labelLarge,
color = textColor,
- modifier = Modifier.padding(horizontal = 8.dp)
+ modifier = Modifier.padding(start = if (compact) 12.dp else 8.dp, end = 8.dp)
)
}
if (clearable) {
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt
index 11c0c698..49a9d59d 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheet.kt
@@ -65,6 +65,7 @@ import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.ui.R
+import de.mm20.launcher2.ui.common.IconPicker
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
@@ -80,7 +81,6 @@ fun CustomizeSearchableSheet(
) {
val viewModel: CustomizeSearchableSheetVM =
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
- val context = LocalContext.current
val pickIcon by viewModel.isIconPickerOpen
@@ -312,194 +312,13 @@ fun CustomizeSearchableSheet(
}
}
} else {
- val iconSize = 48.dp
- val iconSizePx = iconSize.toPixels()
-
- val scope = rememberCoroutineScope()
-
- val suggestions by remember { viewModel.getIconSuggestions(iconSizePx.toInt()) }
- .collectAsState(emptyList())
-
- val defaultIcon by remember {
- viewModel.getDefaultIcon(iconSizePx.toInt())
- }.collectAsState(null)
-
- var query by remember { mutableStateOf("") }
- var filterIconPack by remember { mutableStateOf(null) }
- val isSearching by viewModel.isSearchingIcons
- val iconResults by viewModel.iconSearchResults
-
- var showIconPackFilter by remember { mutableStateOf(false) }
- val installedIconPacks by viewModel.installedIconPacks.collectAsState(null)
- val noPacksInstalled = installedIconPacks?.isEmpty() == true
-
- val columns = LocalGridSettings.current.columnCount
-
- LazyVerticalGrid(
- modifier = Modifier.fillMaxSize(),
- columns = GridCells.Fixed(columns),
+ IconPicker(
+ searchable = searchable,
+ onSelect = {
+ viewModel.pickIcon(it)
+ },
contentPadding = it,
- ) {
-
- item(span = { GridItemSpan(columns) }) {
- OutlinedTextField(
- modifier = Modifier.padding(bottom = 16.dp),
- leadingIcon = {
- Icon(
- imageVector = Icons.Rounded.Search,
- contentDescription = null
- )
- },
- enabled = !noPacksInstalled,
- placeholder = {
- Text(
- stringResource(
- if (noPacksInstalled) R.string.icon_picker_no_packs_installed else R.string.icon_picker_search_icon
- )
- )
- },
- value = query,
- onValueChange = {
- query = it
- scope.launch {
- viewModel.searchIcon(query, filterIconPack)
- }
- },
- singleLine = true,
- )
- }
-
- if (query.isEmpty()) {
- if (defaultIcon != null) {
- item(span = { GridItemSpan(columns) }) {
- Separator(stringResource(R.string.icon_picker_default_icon))
- }
- item {
- IconPreview(item = defaultIcon, iconSize = iconSize, onClick = {
- viewModel.pickIcon(null)
- })
- }
- }
- item(span = { GridItemSpan(columns) }) {
- Separator(stringResource(R.string.icon_picker_suggestions))
- }
-
- items(suggestions) {
- IconPreview(
- it,
- iconSize,
- onClick = { viewModel.pickIcon(it.customIcon) }
- )
- }
- } else {
-
- if (!installedIconPacks.isNullOrEmpty()) {
- item(
- span = { GridItemSpan(columns) },
- ) {
- Button(
- onClick = { showIconPackFilter = !showIconPackFilter },
- modifier = Modifier
- .wrapContentWidth(align = Alignment.CenterHorizontally)
- .padding(bottom = 16.dp),
- contentPadding = PaddingValues(
- horizontal = 16.dp,
- vertical = 8.dp
- )
- ) {
- if (filterIconPack == null) {
- Icon(
- modifier = Modifier
- .padding(end = ButtonDefaults.IconSpacing)
- .size(ButtonDefaults.IconSize),
- imageVector = Icons.Rounded.FilterAlt,
- contentDescription = null
- )
- } else {
- val icon = remember(filterIconPack?.packageName) {
- try {
- filterIconPack?.packageName?.let { pkg ->
- context.packageManager.getApplicationIcon(pkg)
- }
- } catch (e: PackageManager.NameNotFoundException) {
- null
- }
- }
- AsyncImage(
- modifier = Modifier
- .padding(end = ButtonDefaults.IconSpacing)
- .size(ButtonDefaults.IconSize),
- model = icon,
- contentDescription = null
- )
- }
- DropdownMenu(
- expanded = showIconPackFilter,
- onDismissRequest = { showIconPackFilter = false }) {
- DropdownMenuItem(
- text = { Text(stringResource(id = R.string.icon_picker_filter_all_packs)) },
- onClick = {
- showIconPackFilter = false
- filterIconPack = null
- scope.launch {
- viewModel.searchIcon(query, filterIconPack)
- }
- }
- )
- installedIconPacks?.forEach { iconPack ->
- DropdownMenuItem(
- onClick = {
- showIconPackFilter = false
- filterIconPack = iconPack
- scope.launch {
- viewModel.searchIcon(query, filterIconPack)
- }
- },
- text = {
- Text(iconPack.name)
- })
- }
- }
- Text(
- text = filterIconPack?.name
- ?: stringResource(id = R.string.icon_picker_filter_all_packs),
- modifier = Modifier.animateContentSize()
- )
- Icon(
- Icons.Rounded.ArrowDropDown,
- modifier = Modifier
- .padding(start = ButtonDefaults.IconSpacing)
- .size(ButtonDefaults.IconSize),
- contentDescription = null
- )
- }
- }
- }
-
- items(iconResults) {
- IconPreview(
- it,
- iconSize,
- onClick = { viewModel.pickIcon(it.customIcon) }
- )
- }
-
- if (isSearching) {
- item(span = { GridItemSpan(columns) }) {
- Box(
- contentAlignment = Alignment.Center
- ) {
- CircularProgressIndicator(
- modifier = Modifier
- .padding(12.dp)
- .size(24.dp)
- )
- }
- }
- }
- }
-
- }
+ )
}
}
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt
index 2c767b5f..b7ecbacb 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/CustomizeSearchableSheetVM.kt
@@ -35,10 +35,6 @@ class CustomizeSearchableSheetVM(
return iconService.getIcon(searchable, size)
}
- fun getIconSuggestions(size: Int) = flow {
- emit(iconService.getCustomIconSuggestions(searchable, size))
- }
-
fun openIconPicker() {
isIconPickerOpen.value = true
}
@@ -52,34 +48,6 @@ class CustomizeSearchableSheetVM(
closeIconPicker()
}
- fun getDefaultIcon(size: Int) = flow {
- emit(iconService.getUncustomizedDefaultIcon(searchable, size))
- }
-
- val iconSearchResults = mutableStateOf(emptyList())
- val isSearchingIcons = mutableStateOf(false)
-
- val installedIconPacks = iconService.getInstalledIconPacks()
-
- private var debounceSearchJob: Job? = null
- suspend fun searchIcon(query: String, iconPack: IconPack?) {
- debounceSearchJob?.cancelAndJoin()
- if (query.isBlank()) {
- iconSearchResults.value = emptyList()
- isSearchingIcons.value = false
- return
- }
- withContext(coroutineContext) {
- debounceSearchJob = launch {
- delay(500)
- isSearchingIcons.value = true
- iconSearchResults.value = emptyList()
- iconSearchResults.value = iconService.searchCustomIcons(query, iconPack)
- isSearchingIcons.value = false
- }
- }
- }
-
fun setCustomLabel(label: String) {
if (label.isBlank()) {
customAttributesRepository.clearCustomLabel(searchable)
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt
index a6957f6a..20751c5e 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditFavoritesSheet.kt
@@ -91,7 +91,6 @@ import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.ktx.splitLeadingEmoji
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalGridSettings
-import de.mm20.launcher2.ui.settings.tags.EditTagSheet
import kotlin.math.roundToInt
@Composable
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheet.kt
similarity index 82%
rename from app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheet.kt
rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheet.kt
index a50b08c2..63e192f5 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheet.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheet.kt
@@ -1,5 +1,6 @@
-package de.mm20.launcher2.ui.settings.tags
+package de.mm20.launcher2.ui.launcher.sheets
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -22,16 +23,19 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Apps
import androidx.compose.material.icons.rounded.Check
-import androidx.compose.material.icons.rounded.Delete
+import androidx.compose.material.icons.rounded.EmojiEmotions
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -39,6 +43,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -46,13 +51,17 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
+import de.mm20.launcher2.data.customattrs.CustomTextIcon
import de.mm20.launcher2.icons.LauncherIcon
+import de.mm20.launcher2.search.Tag
import de.mm20.launcher2.ui.R
+import de.mm20.launcher2.ui.common.IconPicker
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.SmallMessage
@@ -70,8 +79,10 @@ fun EditTagSheet(
val isCreatingNewTag = tag == null
+ val density = LocalDensity.current
+
LaunchedEffect(tag) {
- viewModel.init(tag)
+ viewModel.init(tag, with(density) { 56.dp.toPx().toInt() })
}
if (viewModel.loading) return
@@ -254,7 +265,7 @@ fun ListItem(
@Composable
fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
val iconSize = 32.dp.toPixels()
- val tagEmoji = viewModel.tagEmoji
+ val tagIcon by remember(viewModel.tagCustomIcon) { viewModel.tagCustomIcon }.collectAsState()
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
@@ -275,11 +286,8 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
}
.size(56.dp)
then (
- if (tagEmoji != null) {
- Modifier.background(
- MaterialTheme.colorScheme.secondaryContainer,
- CircleShape
- )
+ if (tagIcon != null) {
+ Modifier
} else {
Modifier.drawBehind {
val w = with(density) { 2.dp.toPx() }
@@ -300,8 +308,13 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
),
contentAlignment = Alignment.Center,
) {
- if (tagEmoji != null) {
- Text(tagEmoji)
+ if (tagIcon != null) {
+ var icon = remember(viewModel.tagIcon) { viewModel.tagIcon }.collectAsState(null)
+ ShapedLauncherIcon(
+ size = 56.dp,
+ icon = { icon.value },
+ shape = CircleShape,
+ )
} else {
Icon(
Icons.Rounded.Tag,
@@ -395,37 +408,63 @@ fun PickIcon(
viewModel: EditTagSheetVM,
paddingValues: PaddingValues
) {
+ val icon by remember (viewModel.tagCustomIcon) { viewModel.tagCustomIcon }.collectAsState()
+ val tag = Tag(viewModel.tagName)
+ var selectedTabIndex = remember {
+ mutableIntStateOf(
+ when (icon) {
+ is CustomTextIcon -> 1
+ else -> 0
+ }
+ )
+ }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues)
) {
- OutlinedButton(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 16.dp),
- onClick = {
- viewModel.selectIcon(null)
- },
- contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
+ SingleChoiceSegmentedButtonRow(
+ modifier = Modifier.fillMaxWidth(),
) {
- Icon(
- Icons.Rounded.Delete,
- null,
- modifier = Modifier
- .padding(end = ButtonDefaults.IconSpacing)
- .size(ButtonDefaults.IconSize)
+ SegmentedButton(
+ selected = selectedTabIndex.intValue == 0,
+ icon = { Icon(Icons.Rounded.Apps, null) },
+ label = { Text("Icon") },
+ onClick = { selectedTabIndex.intValue = 0 },
+ shape = SegmentedButtonDefaults.itemShape(0, 2)
+ )
+ SegmentedButton(
+ selected = selectedTabIndex.intValue == 1,
+ icon = { Icon(Icons.Rounded.EmojiEmotions, null) },
+ label = { Text("Emoji") },
+ onClick = { selectedTabIndex.intValue = 1 },
+ shape = SegmentedButtonDefaults.itemShape(1, 2)
)
- Text(stringResource(R.string.reset_icon))
}
- EmojiPicker(
- modifier = Modifier
- .fillMaxWidth()
- .weight(1f),
- onEmojiSelected = {
- viewModel.selectIcon(it)
+ AnimatedContent(
+ selectedTabIndex.intValue,
+ modifier = Modifier.padding(top = 16.dp)
+ ) {
+ when (it) {
+ 0 -> {
+ IconPicker(
+ searchable = tag,
+ onSelect = { viewModel.selectIcon(it) },
+ )
+ }
+
+ 1 -> {
+ EmojiPicker(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ onEmojiSelected = {
+ viewModel.selectIcon(CustomTextIcon(text = it))
+ },
+ )
+ }
}
- )
+ }
}
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheetVM.kt
similarity index 83%
rename from app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt
rename to app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheetVM.kt
index b59dc080..52e79187 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/EditTagSheetVM.kt
@@ -1,6 +1,5 @@
-package de.mm20.launcher2.ui.settings.tags
+package de.mm20.launcher2.ui.launcher.sheets
-import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -9,18 +8,21 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository
-import de.mm20.launcher2.data.customattrs.CustomTextIcon
+import de.mm20.launcher2.data.customattrs.CustomIcon
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
-import de.mm20.launcher2.icons.StaticLauncherIcon
-import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Tag
import de.mm20.launcher2.services.tags.TagsService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -35,7 +37,8 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
private var oldTagName by mutableStateOf(null)
private var allTags by mutableStateOf(emptySet())
var tagName by mutableStateOf("")
- var tagEmoji by mutableStateOf(null)
+ var tagCustomIcon = MutableStateFlow(null)
+ var tagIcon = emptyFlow()
var loading by mutableStateOf(true)
@@ -50,7 +53,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
}
- fun init(tag: String?) {
+ fun init(tag: String?, iconSize: Int) {
loading = true
this.oldTagName = tag
this.tagName = tag ?: ""
@@ -59,8 +62,11 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
viewModelScope.launch(Dispatchers.Default) {
allTags = tagService.getAllTags().first().toSet()
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
- val icon = if (tag != null) iconService.getIcon(Tag(tag), 0).first() else null
- tagEmoji = ((icon as? StaticLauncherIcon)?.foregroundLayer as? TextLayer)?.text
+ tagCustomIcon.value = if (tag != null) iconService.getCustomIcon(Tag(tag)).first() else null
+ tagIcon = tagCustomIcon.map {
+ if (tag != null) iconService.resolveCustomIcon(Tag(tag), iconSize, it).first()
+ else null
+ }
val apps = appRepository.findMany().first { it.isNotEmpty() }.sorted()
taggedItems = items
@@ -76,7 +82,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
fun save() {
val oldName = oldTagName
val newName = tagName
- val tagEmoji = tagEmoji
+ val tagIcon = tagCustomIcon
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
else tagService.createTag(tagName, taggedItems)
@@ -84,8 +90,8 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
if (oldName != null && oldName != newName) {
iconService.setCustomIcon(Tag(oldName), null)
}
- if (tagEmoji != null) {
- iconService.setCustomIcon(Tag(newName), CustomTextIcon(tagEmoji))
+ if (tagIcon != null) {
+ iconService.setCustomIcon(Tag(newName), tagCustomIcon.value)
} else {
iconService.setCustomIcon(Tag(newName), null)
}
@@ -114,8 +120,8 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
page = EditTagSheetPage.CustomizeTag
}
- fun selectIcon(emoji: String?) {
- tagEmoji = emoji
+ fun selectIcon(icon: CustomIcon?) {
+ tagCustomIcon.value = icon
closeIconPicker()
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt
index 9f139329..5133cff8 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/sheets/LauncherBottomSheets.kt
@@ -1,7 +1,6 @@
package de.mm20.launcher2.ui.launcher.sheets
import androidx.compose.runtime.Composable
-import de.mm20.launcher2.ui.settings.tags.EditTagSheet
@Composable
fun LauncherBottomSheets() {
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/TagsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/TagsSettingsScreen.kt
index 30045e58..bbd1a941 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/TagsSettingsScreen.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/TagsSettingsScreen.kt
@@ -5,7 +5,6 @@ import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.MoreVert
-import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
@@ -26,7 +25,7 @@ import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
-import de.mm20.launcher2.ui.ktx.splitLeadingEmoji
+import de.mm20.launcher2.ui.launcher.sheets.EditTagSheet
@Composable
fun TagsSettingsScreen() {
diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
index 70797e4e..8b907f7a 100644
--- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
+++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
@@ -38,6 +38,8 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.ui.IconSettings
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.SavableSearchable
+import de.mm20.launcher2.search.Searchable
+import de.mm20.launcher2.search.Tag
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -48,6 +50,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMap
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -143,6 +147,10 @@ class IconService(
}
}
+ fun getCustomIcon(searchable: SavableSearchable) : Flow {
+ return customAttributesRepository.getCustomIcon(searchable)
+ }
+
fun getIcon(searchable: SavableSearchable, size: Int): Flow {
if (searchable is Application && searchable.isPrivate) {
@@ -151,22 +159,29 @@ class IconService(
}
}
- val customIcon = customAttributesRepository.getCustomIcon(searchable)
- return combine(iconProviders, transformations, customIcon) { providers, transformations, ci ->
- var icon = cache.get(searchable.key + ci.hashCode() + providers.hashCode() + transformations.hashCode())
+ val customIcon = getCustomIcon(searchable)
+
+ return customIcon.flatMapLatest {
+ resolveCustomIcon(searchable, size, it)
+ }
+ }
+
+ fun resolveCustomIcon(searchable: SavableSearchable, size: Int, customIcon: CustomIcon?): Flow {
+ return combine(iconProviders, transformations) { providers, transformations ->
+ var icon = cache.get(searchable.key + customIcon.hashCode() + providers.hashCode() + transformations.hashCode())
if (icon != null) {
return@combine icon
}
- val provs = if (ci != null) getProviders(ci) + providers else providers
- val transforms = getTransformations(ci) ?: transformations
+ val provs = if (customIcon != null) getProviders(customIcon) + providers else providers
+ val transforms = getTransformations(customIcon) ?: transformations
icon = provs.getFirstIcon(searchable, size)
if (icon != null) {
icon = icon.transform(transforms)
- cache.put(searchable.key + ci.hashCode() + providers.hashCode() + transformations.hashCode(), icon)
+ cache.put(searchable.key + customIcon.hashCode() + providers.hashCode() + transformations.hashCode(), icon)
}
return@combine icon
}
@@ -262,7 +277,11 @@ class IconService(
val defaultTransformations = transformations.first()
- val transformationOptions = mutableListOf(UnmodifiedSystemDefaultIcon)
+ val transformationOptions = mutableListOf()
+
+ if (searchable is Application) {
+ transformationOptions.add(UnmodifiedSystemDefaultIcon)
+ }
if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) {
// Legacy icons that simply fill the entire canvas
@@ -325,13 +344,11 @@ class IconService(
transformationOptions.add(
ForceThemedIcon
)
- } else {
- transformationOptions.add(
- ForceThemedIcon
- )
}
- providerOptions.add(DefaultPlaceholderIcon)
+ if (searchable !is Tag) {
+ providerOptions.add(DefaultPlaceholderIcon)
+ }
suggestions.addAll(
transformationOptions.map {