Add support for custom icon pack tag icons
This commit is contained in:
parent
25551859c5
commit
ca36f3cac1
3
.idea/inspectionProfiles/Project_Default.xml
generated
3
.idea/inspectionProfiles/Project_Default.xml
generated
@ -65,6 +65,9 @@
|
|||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
|
|||||||
263
app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPicker.kt
Normal file
263
app/ui/src/main/java/de/mm20/launcher2/ui/common/IconPicker.kt
Normal file
@ -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<IconPack?>(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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<CustomIconWithPreview>())
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material3.FilterChipDefaults
|
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.TextLayer
|
||||||
import de.mm20.launcher2.icons.VectorLayer
|
import de.mm20.launcher2.icons.VectorLayer
|
||||||
import de.mm20.launcher2.search.Tag
|
import de.mm20.launcher2.search.Tag
|
||||||
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
import org.koin.androidx.compose.inject
|
import org.koin.androidx.compose.inject
|
||||||
|
|
||||||
@ -113,21 +115,31 @@ fun TagChip(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(start = 4.dp, end = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
val foregroundLayer = (icon as? StaticLauncherIcon)?.foregroundLayer
|
val foregroundLayer = (icon as? StaticLauncherIcon)?.foregroundLayer
|
||||||
AnimatedVisibility(!compact || foregroundLayer is TextLayer) {
|
AnimatedVisibility(!compact || foregroundLayer !is VectorLayer) {
|
||||||
if (foregroundLayer is TextLayer) {
|
if (foregroundLayer is TextLayer) {
|
||||||
Text(
|
Text(
|
||||||
text = foregroundLayer.text,
|
text = foregroundLayer.text,
|
||||||
modifier = Modifier.width(FilterChipDefaults.IconSize),
|
modifier = Modifier
|
||||||
|
.padding(start = 4.dp)
|
||||||
|
.width(FilterChipDefaults.IconSize),
|
||||||
textAlign = TextAlign.Center,
|
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(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.padding(start = 4.dp)
|
||||||
.size(FilterChipDefaults.IconSize),
|
.size(FilterChipDefaults.IconSize),
|
||||||
imageVector = foregroundLayer.vector,
|
imageVector = foregroundLayer.vector,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@ -140,7 +152,7 @@ fun TagChip(
|
|||||||
tag.tag,
|
tag.tag,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
modifier = Modifier.padding(start = if (compact) 12.dp else 8.dp, end = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (clearable) {
|
if (clearable) {
|
||||||
|
|||||||
@ -65,6 +65,7 @@ import de.mm20.launcher2.search.CalendarEvent
|
|||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.searchable.VisibilityLevel
|
import de.mm20.launcher2.searchable.VisibilityLevel
|
||||||
import de.mm20.launcher2.ui.R
|
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.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
|
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
@ -80,7 +81,6 @@ fun CustomizeSearchableSheet(
|
|||||||
) {
|
) {
|
||||||
val viewModel: CustomizeSearchableSheetVM =
|
val viewModel: CustomizeSearchableSheetVM =
|
||||||
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
|
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val pickIcon by viewModel.isIconPickerOpen
|
val pickIcon by viewModel.isIconPickerOpen
|
||||||
|
|
||||||
@ -312,194 +312,13 @@ fun CustomizeSearchableSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val iconSize = 48.dp
|
IconPicker(
|
||||||
val iconSizePx = iconSize.toPixels()
|
searchable = searchable,
|
||||||
|
onSelect = {
|
||||||
val scope = rememberCoroutineScope()
|
viewModel.pickIcon(it)
|
||||||
|
},
|
||||||
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<IconPack?>(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 = 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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,10 +35,6 @@ class CustomizeSearchableSheetVM(
|
|||||||
return iconService.getIcon(searchable, size)
|
return iconService.getIcon(searchable, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getIconSuggestions(size: Int) = flow {
|
|
||||||
emit(iconService.getCustomIconSuggestions(searchable, size))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openIconPicker() {
|
fun openIconPicker() {
|
||||||
isIconPickerOpen.value = true
|
isIconPickerOpen.value = true
|
||||||
}
|
}
|
||||||
@ -52,34 +48,6 @@ class CustomizeSearchableSheetVM(
|
|||||||
closeIconPicker()
|
closeIconPicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefaultIcon(size: Int) = flow {
|
|
||||||
emit(iconService.getUncustomizedDefaultIcon(searchable, size))
|
|
||||||
}
|
|
||||||
|
|
||||||
val iconSearchResults = mutableStateOf(emptyList<CustomIconWithPreview>())
|
|
||||||
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) {
|
fun setCustomLabel(label: String) {
|
||||||
if (label.isBlank()) {
|
if (label.isBlank()) {
|
||||||
customAttributesRepository.clearCustomLabel(searchable)
|
customAttributesRepository.clearCustomLabel(searchable)
|
||||||
|
|||||||
@ -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.splitLeadingEmoji
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
import de.mm20.launcher2.ui.settings.tags.EditTagSheet
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@ -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.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
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.text.KeyboardActions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
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.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.Tag
|
||||||
import androidx.compose.material.icons.rounded.Warning
|
import androidx.compose.material.icons.rounded.Warning
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
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.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@ -39,6 +43,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.draw.drawBehind
|
||||||
import androidx.compose.ui.graphics.PathEffect
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.data.customattrs.CustomTextIcon
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
|
import de.mm20.launcher2.search.Tag
|
||||||
import de.mm20.launcher2.ui.R
|
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.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.component.SmallMessage
|
import de.mm20.launcher2.ui.component.SmallMessage
|
||||||
@ -70,8 +79,10 @@ fun EditTagSheet(
|
|||||||
|
|
||||||
val isCreatingNewTag = tag == null
|
val isCreatingNewTag = tag == null
|
||||||
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
LaunchedEffect(tag) {
|
LaunchedEffect(tag) {
|
||||||
viewModel.init(tag)
|
viewModel.init(tag, with(density) { 56.dp.toPx().toInt() })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.loading) return
|
if (viewModel.loading) return
|
||||||
@ -254,7 +265,7 @@ fun ListItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
||||||
val iconSize = 32.dp.toPixels()
|
val iconSize = 32.dp.toPixels()
|
||||||
val tagEmoji = viewModel.tagEmoji
|
val tagIcon by remember(viewModel.tagCustomIcon) { viewModel.tagCustomIcon }.collectAsState()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
@ -275,11 +286,8 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
|||||||
}
|
}
|
||||||
.size(56.dp)
|
.size(56.dp)
|
||||||
then (
|
then (
|
||||||
if (tagEmoji != null) {
|
if (tagIcon != null) {
|
||||||
Modifier.background(
|
Modifier
|
||||||
MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
CircleShape
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Modifier.drawBehind {
|
Modifier.drawBehind {
|
||||||
val w = with(density) { 2.dp.toPx() }
|
val w = with(density) { 2.dp.toPx() }
|
||||||
@ -300,8 +308,13 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
|||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
if (tagEmoji != null) {
|
if (tagIcon != null) {
|
||||||
Text(tagEmoji)
|
var icon = remember(viewModel.tagIcon) { viewModel.tagIcon }.collectAsState(null)
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
size = 56.dp,
|
||||||
|
icon = { icon.value },
|
||||||
|
shape = CircleShape,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Rounded.Tag,
|
Icons.Rounded.Tag,
|
||||||
@ -395,37 +408,63 @@ fun PickIcon(
|
|||||||
viewModel: EditTagSheetVM,
|
viewModel: EditTagSheetVM,
|
||||||
paddingValues: PaddingValues
|
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(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
SingleChoiceSegmentedButtonRow(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
onClick = {
|
|
||||||
viewModel.selectIcon(null)
|
|
||||||
},
|
|
||||||
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
SegmentedButton(
|
||||||
Icons.Rounded.Delete,
|
selected = selectedTabIndex.intValue == 0,
|
||||||
null,
|
icon = { Icon(Icons.Rounded.Apps, null) },
|
||||||
modifier = Modifier
|
label = { Text("Icon") },
|
||||||
.padding(end = ButtonDefaults.IconSpacing)
|
onClick = { selectedTabIndex.intValue = 0 },
|
||||||
.size(ButtonDefaults.IconSize)
|
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(
|
AnimatedContent(
|
||||||
modifier = Modifier
|
selectedTabIndex.intValue,
|
||||||
.fillMaxWidth()
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
.weight(1f),
|
) {
|
||||||
onEmojiSelected = {
|
when (it) {
|
||||||
viewModel.selectIcon(it)
|
0 -> {
|
||||||
|
IconPicker(
|
||||||
|
searchable = tag,
|
||||||
|
onSelect = { viewModel.selectIcon(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
EmojiPicker(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
onEmojiSelected = {
|
||||||
|
viewModel.selectIcon(CustomTextIcon(text = it))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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.Stable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -9,18 +8,21 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.applications.AppRepository
|
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.IconService
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
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.SavableSearchable
|
||||||
import de.mm20.launcher2.search.SearchService
|
import de.mm20.launcher2.search.SearchService
|
||||||
import de.mm20.launcher2.search.Tag
|
import de.mm20.launcher2.search.Tag
|
||||||
import de.mm20.launcher2.services.tags.TagsService
|
import de.mm20.launcher2.services.tags.TagsService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.first
|
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 kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
@ -35,7 +37,8 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
private var oldTagName by mutableStateOf<String?>(null)
|
private var oldTagName by mutableStateOf<String?>(null)
|
||||||
private var allTags by mutableStateOf(emptySet<String>())
|
private var allTags by mutableStateOf(emptySet<String>())
|
||||||
var tagName by mutableStateOf("")
|
var tagName by mutableStateOf("")
|
||||||
var tagEmoji by mutableStateOf<String?>(null)
|
var tagCustomIcon = MutableStateFlow<CustomIcon?>(null)
|
||||||
|
var tagIcon = emptyFlow<LauncherIcon?>()
|
||||||
|
|
||||||
var loading by mutableStateOf(true)
|
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
|
loading = true
|
||||||
this.oldTagName = tag
|
this.oldTagName = tag
|
||||||
this.tagName = tag ?: ""
|
this.tagName = tag ?: ""
|
||||||
@ -59,8 +62,11 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
allTags = tagService.getAllTags().first().toSet()
|
allTags = tagService.getAllTags().first().toSet()
|
||||||
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
|
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
|
||||||
val icon = if (tag != null) iconService.getIcon(Tag(tag), 0).first() else null
|
tagCustomIcon.value = if (tag != null) iconService.getCustomIcon(Tag(tag)).first() else null
|
||||||
tagEmoji = ((icon as? StaticLauncherIcon)?.foregroundLayer as? TextLayer)?.text
|
tagIcon = tagCustomIcon.map {
|
||||||
|
if (tag != null) iconService.resolveCustomIcon(Tag(tag), iconSize, it).first()
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
val apps = appRepository.findMany().first { it.isNotEmpty() }.sorted()
|
val apps = appRepository.findMany().first { it.isNotEmpty() }.sorted()
|
||||||
taggedItems = items
|
taggedItems = items
|
||||||
@ -76,7 +82,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
fun save() {
|
fun save() {
|
||||||
val oldName = oldTagName
|
val oldName = oldTagName
|
||||||
val newName = tagName
|
val newName = tagName
|
||||||
val tagEmoji = tagEmoji
|
val tagIcon = tagCustomIcon
|
||||||
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
|
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
|
||||||
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
|
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
|
||||||
else tagService.createTag(tagName, taggedItems)
|
else tagService.createTag(tagName, taggedItems)
|
||||||
@ -84,8 +90,8 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
if (oldName != null && oldName != newName) {
|
if (oldName != null && oldName != newName) {
|
||||||
iconService.setCustomIcon(Tag(oldName), null)
|
iconService.setCustomIcon(Tag(oldName), null)
|
||||||
}
|
}
|
||||||
if (tagEmoji != null) {
|
if (tagIcon != null) {
|
||||||
iconService.setCustomIcon(Tag(newName), CustomTextIcon(tagEmoji))
|
iconService.setCustomIcon(Tag(newName), tagCustomIcon.value)
|
||||||
} else {
|
} else {
|
||||||
iconService.setCustomIcon(Tag(newName), null)
|
iconService.setCustomIcon(Tag(newName), null)
|
||||||
}
|
}
|
||||||
@ -114,8 +120,8 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
page = EditTagSheetPage.CustomizeTag
|
page = EditTagSheetPage.CustomizeTag
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectIcon(emoji: String?) {
|
fun selectIcon(icon: CustomIcon?) {
|
||||||
tagEmoji = emoji
|
tagCustomIcon.value = icon
|
||||||
closeIconPicker()
|
closeIconPicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.sheets
|
package de.mm20.launcher2.ui.launcher.sheets
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import de.mm20.launcher2.ui.settings.tags.EditTagSheet
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LauncherBottomSheets() {
|
fun LauncherBottomSheets() {
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import androidx.compose.material.icons.rounded.Add
|
|||||||
import androidx.compose.material.icons.rounded.ContentCopy
|
import androidx.compose.material.icons.rounded.ContentCopy
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
import androidx.compose.material.icons.rounded.MoreVert
|
import androidx.compose.material.icons.rounded.MoreVert
|
||||||
import androidx.compose.material.icons.rounded.Tag
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.FloatingActionButton
|
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.Preference
|
||||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||||
import de.mm20.launcher2.ui.ktx.splitLeadingEmoji
|
import de.mm20.launcher2.ui.launcher.sheets.EditTagSheet
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TagsSettingsScreen() {
|
fun TagsSettingsScreen() {
|
||||||
|
|||||||
@ -38,6 +38,8 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
|||||||
import de.mm20.launcher2.preferences.ui.IconSettings
|
import de.mm20.launcher2.preferences.ui.IconSettings
|
||||||
import de.mm20.launcher2.search.Application
|
import de.mm20.launcher2.search.Application
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -48,6 +50,8 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMap
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -143,6 +147,10 @@ class IconService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCustomIcon(searchable: SavableSearchable) : Flow<CustomIcon?> {
|
||||||
|
return customAttributesRepository.getCustomIcon(searchable)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {
|
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {
|
||||||
if (searchable is Application && searchable.isPrivate) {
|
if (searchable is Application && searchable.isPrivate) {
|
||||||
@ -151,22 +159,29 @@ class IconService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val customIcon = customAttributesRepository.getCustomIcon(searchable)
|
val customIcon = getCustomIcon(searchable)
|
||||||
return combine(iconProviders, transformations, customIcon) { providers, transformations, ci ->
|
|
||||||
var icon = cache.get(searchable.key + ci.hashCode() + providers.hashCode() + transformations.hashCode())
|
return customIcon.flatMapLatest {
|
||||||
|
resolveCustomIcon(searchable, size, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolveCustomIcon(searchable: SavableSearchable, size: Int, customIcon: CustomIcon?): Flow<LauncherIcon> {
|
||||||
|
return combine(iconProviders, transformations) { providers, transformations ->
|
||||||
|
var icon = cache.get(searchable.key + customIcon.hashCode() + providers.hashCode() + transformations.hashCode())
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
return@combine icon
|
return@combine icon
|
||||||
}
|
}
|
||||||
|
|
||||||
val provs = if (ci != null) getProviders(ci) + providers else providers
|
val provs = if (customIcon != null) getProviders(customIcon) + providers else providers
|
||||||
val transforms = getTransformations(ci) ?: transformations
|
val transforms = getTransformations(customIcon) ?: transformations
|
||||||
|
|
||||||
icon = provs.getFirstIcon(searchable, size)
|
icon = provs.getFirstIcon(searchable, size)
|
||||||
|
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
icon = icon.transform(transforms)
|
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
|
return@combine icon
|
||||||
}
|
}
|
||||||
@ -262,7 +277,11 @@ class IconService(
|
|||||||
|
|
||||||
val defaultTransformations = transformations.first()
|
val defaultTransformations = transformations.first()
|
||||||
|
|
||||||
val transformationOptions = mutableListOf<CustomIcon>(UnmodifiedSystemDefaultIcon)
|
val transformationOptions = mutableListOf<CustomIcon>()
|
||||||
|
|
||||||
|
if (searchable is Application) {
|
||||||
|
transformationOptions.add(UnmodifiedSystemDefaultIcon)
|
||||||
|
}
|
||||||
|
|
||||||
if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) {
|
if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) {
|
||||||
// Legacy icons that simply fill the entire canvas
|
// Legacy icons that simply fill the entire canvas
|
||||||
@ -325,13 +344,11 @@ class IconService(
|
|||||||
transformationOptions.add(
|
transformationOptions.add(
|
||||||
ForceThemedIcon
|
ForceThemedIcon
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
transformationOptions.add(
|
|
||||||
ForceThemedIcon
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
providerOptions.add(DefaultPlaceholderIcon)
|
if (searchable !is Tag) {
|
||||||
|
providerOptions.add(DefaultPlaceholderIcon)
|
||||||
|
}
|
||||||
|
|
||||||
suggestions.addAll(
|
suggestions.addAll(
|
||||||
transformationOptions.map {
|
transformationOptions.map {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user