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
|
package de.mm20.launcher2.ui.launcher.sheets
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.InsetDrawable
|
import android.graphics.drawable.InsetDrawable
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
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.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material.icons.Icons
|
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.material.icons.rounded.Search
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.*
|
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.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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.toArgb
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
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.badges.Badge
|
||||||
import de.mm20.launcher2.icons.CustomIconWithPreview
|
import de.mm20.launcher2.icons.CustomIconWithPreview
|
||||||
|
import de.mm20.launcher2.icons.IconPack
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
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.OutlinedTagsInputField
|
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.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@ -42,7 +66,7 @@ fun CustomizeSearchableSheet(
|
|||||||
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
|
remember(searchable.key) { CustomizeSearchableSheetVM(searchable) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val pickIcon by viewModel.isIconPickerOpen.observeAsState(false)
|
val pickIcon by viewModel.isIconPickerOpen
|
||||||
|
|
||||||
BottomSheetDialog(
|
BottomSheetDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@ -156,8 +180,13 @@ fun CustomizeSearchableSheet(
|
|||||||
}.observeAsState()
|
}.observeAsState()
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
val isSearching by viewModel.isSearchingIcons.observeAsState(initial = false)
|
var filterIconPack by remember { mutableStateOf<IconPack?>(null) }
|
||||||
val iconResults by viewModel.iconSearchResults.observeAsState(emptyList())
|
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
|
val columns = LocalGridSettings.current.columnCount
|
||||||
|
|
||||||
@ -176,18 +205,67 @@ fun CustomizeSearchableSheet(
|
|||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
enabled = !noPacksInstalled,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
if (noPacksInstalled) R.string.icon_picker_no_packs_installed else R.string.icon_picker_search_icon
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
if (query.isNotEmpty()) {
|
if (query.isNotEmpty() && !installedIconPacks.isNullOrEmpty()) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
query = ""
|
showIconPackFilter = !showIconPackFilter
|
||||||
scope.launch {
|
|
||||||
viewModel.searchIcon("")
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
if (filterIconPack == null) {
|
||||||
imageVector = Icons.Rounded.Clear,
|
Icon(
|
||||||
contentDescription = null
|
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 = {
|
onValueChange = {
|
||||||
query = it
|
query = it
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.searchIcon(query)
|
viewModel.searchIcon(query, filterIconPack)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = {
|
|
||||||
Text(stringResource(R.string.icon_picker_search_icon))
|
|
||||||
},
|
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.sheets
|
package de.mm20.launcher2.ui.launcher.sheets
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
|
||||||
import de.mm20.launcher2.data.customattrs.CustomIcon
|
import de.mm20.launcher2.data.customattrs.CustomIcon
|
||||||
import de.mm20.launcher2.icons.CustomIconWithPreview
|
import de.mm20.launcher2.icons.CustomIconWithPreview
|
||||||
|
import de.mm20.launcher2.icons.IconPack
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.icons.IconRepository
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
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.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
@ -21,7 +27,7 @@ class CustomizeSearchableSheetVM(
|
|||||||
private val iconRepository: IconRepository by inject()
|
private val iconRepository: IconRepository by inject()
|
||||||
private val customAttributesRepository: CustomAttributesRepository by inject()
|
private val customAttributesRepository: CustomAttributesRepository by inject()
|
||||||
|
|
||||||
val isIconPickerOpen = MutableLiveData(false)
|
val isIconPickerOpen = mutableStateOf(false)
|
||||||
|
|
||||||
fun getIcon(size: Int): Flow<LauncherIcon> {
|
fun getIcon(size: Int): Flow<LauncherIcon> {
|
||||||
return iconRepository.getIcon(searchable, size)
|
return iconRepository.getIcon(searchable, size)
|
||||||
@ -48,11 +54,15 @@ class CustomizeSearchableSheetVM(
|
|||||||
emit(iconRepository.getUncustomizedDefaultIcon(searchable, size))
|
emit(iconRepository.getUncustomizedDefaultIcon(searchable, size))
|
||||||
}
|
}
|
||||||
|
|
||||||
val iconSearchResults = MutableLiveData(emptyList<CustomIconWithPreview>())
|
val iconSearchResults = mutableStateOf(emptyList<CustomIconWithPreview>())
|
||||||
val isSearchingIcons = MutableLiveData(false)
|
val isSearchingIcons = mutableStateOf(false)
|
||||||
|
|
||||||
|
val installedIconPacks = flow {
|
||||||
|
emit(iconRepository.getInstalledIconPacks().sortedBy { it.name })
|
||||||
|
}
|
||||||
|
|
||||||
private var debounceSearchJob: Job? = null
|
private var debounceSearchJob: Job? = null
|
||||||
suspend fun searchIcon(query: String) {
|
suspend fun searchIcon(query: String, iconPack: IconPack?) {
|
||||||
debounceSearchJob?.cancelAndJoin()
|
debounceSearchJob?.cancelAndJoin()
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
iconSearchResults.value = emptyList()
|
iconSearchResults.value = emptyList()
|
||||||
@ -61,9 +71,10 @@ class CustomizeSearchableSheetVM(
|
|||||||
}
|
}
|
||||||
withContext(coroutineContext) {
|
withContext(coroutineContext) {
|
||||||
debounceSearchJob = launch {
|
debounceSearchJob = launch {
|
||||||
delay(1000)
|
delay(500)
|
||||||
isSearchingIcons.value = true
|
isSearchingIcons.value = true
|
||||||
iconSearchResults.value = iconRepository.searchCustomIcons(query)
|
iconSearchResults.value = emptyList()
|
||||||
|
iconSearchResults.value = iconRepository.searchCustomIcons(query, iconPack)
|
||||||
isSearchingIcons.value = false
|
isSearchingIcons.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,14 @@ interface IconDao {
|
|||||||
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')")
|
@Query("SELECT * FROM Icons WHERE componentName = :componentName AND (type = 'app' OR type = 'calendar')")
|
||||||
suspend fun getIconsFromAllPacks(componentName: String): List<IconEntity>
|
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")
|
@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, limit: Int = 100): List<IconEntity>
|
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")
|
@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>
|
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_default_icon">Default</string>
|
||||||
<string name="icon_picker_suggestions">Suggestions</string>
|
<string name="icon_picker_suggestions">Suggestions</string>
|
||||||
<string name="icon_picker_search_icon">Search icon</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_main">Personal</string>
|
||||||
<string name="apps_profile_work">Work</string>
|
<string name="apps_profile_work">Work</string>
|
||||||
<string name="favorites">Favorites</string>
|
<string name="favorites">Favorites</string>
|
||||||
|
|||||||
@ -427,13 +427,14 @@ class IconPackManager(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
|
suspend fun searchIconPackIcon(query: String, iconPack: IconPack?): List<IconPackIcon> {
|
||||||
val iconDao = appDatabase.iconDao()
|
val iconDao = appDatabase.iconDao()
|
||||||
val drawableQuery = query.replace(" ", "_").lowercase()
|
val drawableQuery = query.replace(" ", "_").lowercase()
|
||||||
return iconDao.searchIconPackIcons(
|
return iconDao.searchIconPackIcons(
|
||||||
drawableQuery = "%$drawableQuery%",
|
drawableQuery = "%$drawableQuery%",
|
||||||
componentQuery = "%$query%",
|
componentQuery = "%$query%",
|
||||||
nameQuery = "%$query%",
|
nameQuery = "%$query%",
|
||||||
|
iconPack = iconPack?.packageName,
|
||||||
).map {
|
).map {
|
||||||
IconPackIcon(it)
|
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 transformations = this.transformations.first()
|
||||||
val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull {
|
val iconPackIcons = iconPackManager.searchIconPackIcon(query, iconPack).mapNotNull {
|
||||||
val componentName = it.componentName ?: return@mapNotNull null
|
val componentName = it.componentName ?: return@mapNotNull null
|
||||||
|
|
||||||
CustomIconWithPreview(
|
CustomIconWithPreview(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user