Icon picker: add icon pack filter

This commit is contained in:
MM20 2023-02-27 14:39:18 +01:00
parent 81c542d777
commit 6adbee224e
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
6 changed files with 128 additions and 33 deletions

View File

@ -1,18 +1,40 @@
package de.mm20.launcher2.ui.launcher.sheets
import android.content.pm.PackageManager
import android.graphics.drawable.InsetDrawable
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.Clear
import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
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.graphics.toArgb
@ -21,13 +43,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalGridSettings
import kotlinx.coroutines.flow.first
@ -42,7 +66,7 @@ fun CustomizeSearchableSheet(
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
val context = LocalContext.current
val pickIcon by viewModel.isIconPickerOpen.observeAsState(false)
val pickIcon by viewModel.isIconPickerOpen
BottomSheetDialog(
onDismissRequest = onDismiss,
@ -156,8 +180,13 @@ fun CustomizeSearchableSheet(
}.observeAsState()
var query by remember { mutableStateOf("") }
val isSearching by viewModel.isSearchingIcons.observeAsState(initial = false)
val iconResults by viewModel.iconSearchResults.observeAsState(emptyList())
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
@ -176,18 +205,67 @@ fun CustomizeSearchableSheet(
contentDescription = null
)
},
enabled = !noPacksInstalled,
placeholder = {
Text(
stringResource(
if (noPacksInstalled) R.string.icon_picker_no_packs_installed else R.string.icon_picker_search_icon
)
)
},
trailingIcon = {
if (query.isNotEmpty()) {
if (query.isNotEmpty() && !installedIconPacks.isNullOrEmpty()) {
IconButton(onClick = {
query = ""
scope.launch {
viewModel.searchIcon("")
}
showIconPackFilter = !showIconPackFilter
}) {
Icon(
imageVector = Icons.Rounded.Clear,
contentDescription = null
if (filterIconPack == null) {
Icon(
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.size(24.dp),
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)
})
}
}
}
},
@ -195,12 +273,9 @@ fun CustomizeSearchableSheet(
onValueChange = {
query = it
scope.launch {
viewModel.searchIcon(query)
viewModel.searchIcon(query, filterIconPack)
}
},
label = {
Text(stringResource(R.string.icon_picker_search_icon))
},
singleLine = true,
)
}

View File

@ -1,16 +1,22 @@
package de.mm20.launcher2.ui.launcher.sheets
import androidx.lifecycle.MutableLiveData
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.liveData
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.data.customattrs.CustomIcon
import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.coroutines.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
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
@ -21,7 +27,7 @@ class CustomizeSearchableSheetVM(
private val iconRepository: IconRepository by inject()
private val customAttributesRepository: CustomAttributesRepository by inject()
val isIconPickerOpen = MutableLiveData(false)
val isIconPickerOpen = mutableStateOf(false)
fun getIcon(size: Int): Flow<LauncherIcon> {
return iconRepository.getIcon(searchable, size)
@ -48,11 +54,15 @@ class CustomizeSearchableSheetVM(
emit(iconRepository.getUncustomizedDefaultIcon(searchable, size))
}
val iconSearchResults = MutableLiveData(emptyList<CustomIconWithPreview>())
val isSearchingIcons = MutableLiveData(false)
val iconSearchResults = mutableStateOf(emptyList<CustomIconWithPreview>())
val isSearchingIcons = mutableStateOf(false)
val installedIconPacks = flow {
emit(iconRepository.getInstalledIconPacks().sortedBy { it.name })
}
private var debounceSearchJob: Job? = null
suspend fun searchIcon(query: String) {
suspend fun searchIcon(query: String, iconPack: IconPack?) {
debounceSearchJob?.cancelAndJoin()
if (query.isBlank()) {
iconSearchResults.value = emptyList()
@ -61,9 +71,10 @@ class CustomizeSearchableSheetVM(
}
withContext(coroutineContext) {
debounceSearchJob = launch {
delay(1000)
delay(500)
isSearchingIcons.value = true
iconSearchResults.value = iconRepository.searchCustomIcons(query)
iconSearchResults.value = emptyList()
iconSearchResults.value = iconRepository.searchCustomIcons(query, iconPack)
isSearchingIcons.value = false
}
}

View File

@ -19,8 +19,14 @@ interface IconDao {
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')")
suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity>
@Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND (drawable LIKE :drawableQuery OR componentName LIKE :componentQuery OR name LIKE :nameQuery) ORDER BY iconPack, drawable LIMIT :limit")
suspend fun searchIconPackIcons(componentQuery: String, nameQuery: String, drawableQuery: String, limit: Int = 100): List<IconEntity>
@Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND (drawable LIKE :drawableQuery OR componentName LIKE :componentQuery OR name LIKE :nameQuery) AND (:iconPack IS NULL OR iconPack = :iconPack) ORDER BY iconPack, drawable LIMIT :limit")
suspend fun searchIconPackIcons(
componentQuery: String,
nameQuery: String,
drawableQuery: String,
iconPack: String?,
limit: Int = 100
): List<IconEntity>
@Query("SELECT * FROM Icons WHERE (type = 'greyscale_icon') AND componentName LIKE :query GROUP BY componentName ORDER BY drawable LIMIT :limit")
suspend fun searchGreyscaleIcons(query: String, limit: Int = 100): List<IconEntity>

View File

@ -681,6 +681,8 @@
<string name="icon_picker_default_icon">Default</string>
<string name="icon_picker_suggestions">Suggestions</string>
<string name="icon_picker_search_icon">Search icon</string>
<string name="icon_picker_no_packs_installed">No icon packs installed</string>
<string name="icon_picker_filter_all_packs">All icon packs</string>
<string name="apps_profile_main">Personal</string>
<string name="apps_profile_work">Work</string>
<string name="favorites">Favorites</string>

View File

@ -427,13 +427,14 @@ class IconPackManager(
return null
}
suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
val drawableQuery = query.replace(" ", "_").lowercase()
return iconDao.searchIconPackIcons(
drawableQuery = "%$drawableQuery%",
componentQuery = "%$query%",
nameQuery = "%$query%",
iconPack = iconPack?.packageName,
).map {
IconPackIcon(it)
}

View File

@ -356,9 +356,9 @@ class IconRepository(
)
}
suspend fun searchCustomIcons(query: String): List<CustomIconWithPreview> {
suspend fun searchCustomIcons(query: String, iconPack: IconPack?): List<CustomIconWithPreview> {
val transformations = this.transformations.first()
val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull {
val iconPackIcons = iconPackManager.searchIconPackIcon(query, iconPack).mapNotNull {
val componentName = it.componentName ?: return@mapNotNull null
CustomIconWithPreview(