Icon picker: add icon pack filter
This commit is contained in:
parent
81c542d777
commit
6adbee224e
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user