Improve icon picker, add icon search

This commit is contained in:
MM20 2022-07-31 14:36:49 +02:00
parent 050e8284e7
commit a1e8c20b2b
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
11 changed files with 209 additions and 178 deletions

View File

@ -19,8 +19,8 @@ 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 iconPack = :iconPack AND (type = 'app' OR type = 'calendar') LIMIT :limit OFFSET :offset")
suspend fun getIcons(iconPack: String, offset: Int, limit: Int): List<IconEntity>
@Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND drawable LIKE :query ORDER BY iconPack, drawable LIMIT :limit")
suspend fun searchIconPackIcons(query: String, limit: Int = 100): List<IconEntity>
@Query("DELETE FROM Icons WHERE iconPack = :iconPack")
fun deleteIcons(iconPack: String)

View File

@ -623,4 +623,8 @@
<string name="restore_complete">The backup has been restored.</string>
<string name="icon_picker_title">Pick icon</string>
<string name="icon_picker_default_icon">Default</string>
<string name="icon_picker_suggestions">Suggestions</string>
<string name="icon_picker_packs">Icon packs</string>
<string name="icon_picker_search_icon">Search icon</string>
</resources>

View File

@ -40,8 +40,6 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.palette)
implementation(libs.androidx.paging.common)
implementation(libs.materialcomponents.core)
implementation(libs.bundles.androidx.lifecycle)

View File

@ -15,6 +15,7 @@ import android.util.Log
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.customattrs.CustomIconPackIcon
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.ktx.randomElementOrNull
import kotlinx.coroutines.Dispatchers
@ -185,18 +186,12 @@ class IconPackManager(
)
}
suspend fun getIcons(componentName: ComponentName): List<IconPackIcon> {
suspend fun getAllIconPackIcons(componentName: ComponentName): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.getIconsFromAllPacks(componentName.flattenToString())
.map { IconPackIcon(it) }
}
suspend fun getIcons(iconPack: String, offset: Int, limit: Int): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.getIcons(iconPack, offset, limit)
.map { IconPackIcon(it) }
}
private suspend fun getIconBack(iconPack: String): String? {
val iconDao = appDatabase.iconDao()
val iconbacks = iconDao.getIconBacks(iconPack)
@ -242,6 +237,13 @@ class IconPackManager(
)
}
suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
val iconDao = appDatabase.iconDao()
return iconDao.searchIconPackIcons("%$query%").map {
IconPackIcon(it)
}
}
}

View File

@ -1,54 +0,0 @@
package de.mm20.launcher2.icons
import androidx.paging.PagingSource
import androidx.paging.PagingState
import de.mm20.launcher2.customattrs.CustomIconPackIcon
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import de.mm20.launcher2.icons.transformations.apply
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class IconPackPagingSource(
private val iconPackManager: IconPackManager,
private val iconPack: String,
private val transformations: List<LauncherIconTransformation>
) : PagingSource<Int, CustomIconWithPreview>() {
override fun getRefreshKey(state: PagingState<Int, CustomIconWithPreview>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomIconWithPreview> {
val page = params.key ?: 0
val icons = withContext(Dispatchers.IO) {
iconPackManager.getIcons(iconPack, page, page + params.loadSize)
}
val customIcons = mutableListOf<CustomIconWithPreview>()
withContext(Dispatchers.Default) {
for (icon in icons) {
val data = CustomIconPackIcon(iconPack, icon.componentName?.flattenToString() ?: continue)
val ic = iconPackManager.getIcon(
iconPack,
icon.componentName
) ?: continue
customIcons.add(
CustomIconWithPreview(
preview = transformations.apply(ic),
customIcon = data,
)
)
}
}
return LoadResult.Page(
data = customIcons,
prevKey = if (page > 0) page - 1 else null,
nextKey = if (icons.size == params.loadSize) page + 1 else null
)
}
}

View File

@ -6,12 +6,11 @@ import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.util.LruCache
import androidx.paging.PagingSource
import de.mm20.launcher2.customattrs.*
import de.mm20.launcher2.icons.providers.*
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
import de.mm20.launcher2.icons.transformations.apply
import de.mm20.launcher2.icons.transformations.transform
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
@ -117,7 +116,7 @@ class IconRepository(
icon = provs.getFirstIcon(searchable, size)
if (icon != null) {
icon = transforms.apply(icon)
icon = icon.transform(transforms)
cache.put(searchable.key + customIcon.hashCode(), icon)
send(icon)
@ -181,15 +180,6 @@ class IconRepository(
val defaultTransformations = transformations.first()
val defaultTransformedIcon = defaultTransformations.apply(rawIcon)
suggestions.add(
CustomIconWithPreview(
defaultTransformedIcon,
null,
)
)
val customIcons = mutableListOf<CustomIcon>(UnmodifiedSystemDefaultIcon)
if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) {
@ -230,7 +220,7 @@ class IconRepository(
val icon = providers.getFirstIcon(searchable, size) ?: rawIcon
CustomIconWithPreview(
preview = transformations.apply(icon),
preview = icon.transform(transformations),
customIcon = it,
)
@ -240,7 +230,7 @@ class IconRepository(
val providerOptions = mutableListOf<CustomIcon>()
if (searchable is LauncherApp) {
val iconPackIcons = iconPackManager.getIcons(
val iconPackIcons = iconPackManager.getAllIconPackIcons(
searchable.launcherActivityInfo.componentName
)
@ -262,7 +252,7 @@ class IconRepository(
val icon = providers.getFirstIcon(searchable, size) ?: return@mapNotNull null
CustomIconWithPreview(
preview = defaultTransformations.apply(icon),
preview = icon.transform(defaultTransformations),
customIcon = it,
)
@ -273,8 +263,28 @@ class IconRepository(
}
suspend fun getAllIconsFromPack(iconPack: String): PagingSource<Int, CustomIconWithPreview> {
return IconPackPagingSource(iconPackManager, iconPack, transformations.first())
suspend fun getUncustomizedDefaultIcon(searchable: Searchable, size: Int): CustomIconWithPreview? {
val icon = iconProviders.first().getFirstIcon(searchable, size)
?.transform(transformations.first()) ?: return null
return CustomIconWithPreview(
customIcon = null,
preview = icon
)
}
suspend fun searchIconPackIcon(query: String): List<CustomIconWithPreview> {
val transformations = this.transformations.first()
return iconPackManager.searchIconPackIcon(query).mapNotNull {
val componentName = it.componentName ?: return@mapNotNull null
CustomIconWithPreview(
customIcon = CustomIconPackIcon(
iconPackPackage = it.iconPack,
iconComponentName = componentName.flattenToString(),
),
preview = iconPackManager.getIcon(it.iconPack, componentName)?.transform(transformations) ?: return@mapNotNull null
)
}
}
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) {

View File

@ -8,17 +8,17 @@ internal interface LauncherIconTransformation {
suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon
}
internal suspend fun Iterable<LauncherIconTransformation>.apply(icon: LauncherIcon): LauncherIcon {
if (icon is StaticLauncherIcon) {
var transformedIcon = icon
for (transformation in this) {
internal suspend fun LauncherIcon.transform(transformations: Iterable<LauncherIconTransformation>): LauncherIcon {
if (this is StaticLauncherIcon) {
var transformedIcon = this
for (transformation in transformations) {
transformedIcon = transformation.transform(transformedIcon as StaticLauncherIcon)
}
return transformedIcon
}
if (icon is TransformableDynamicLauncherIcon) {
icon.setTransformations(this.toList())
return icon
if (this is TransformableDynamicLauncherIcon) {
this.setTransformations(transformations.toList())
return this
}
return icon
return this
}

View File

@ -238,14 +238,6 @@ dependencyResolutionManagement {
.to("androidx.navigation", "navigation-compose")
.version("2.5.0-rc02")
alias("androidx.paging.common")
.to("androidx.paging", "paging-common-ktx")
.version("3.2.0-alpha01")
alias("androidx.paging.compose")
.to("androidx.paging", "paging-compose")
.version("1.0.0-alpha15")
alias("materialcomponents.core")
.to("com.google.android.material", "material")
.version("1.6.0-beta01")

View File

@ -64,7 +64,6 @@ dependencies {
implementation(libs.androidx.compose.animationgraphics)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.paging.compose)
implementation(libs.composecolorpicker)

View File

@ -2,41 +2,34 @@ package de.mm20.launcher2.ui.launcher.search.common.customattrs
import android.graphics.drawable.InsetDrawable
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.paging.compose.itemsIndexed
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Clear
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
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 androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.search.data.Searchable
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.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalGridColumns
import kotlinx.coroutines.launch
@Composable
fun CustomizeSearchableSheet(
@ -111,69 +104,146 @@ fun CustomizeSearchableSheet(
} else {
val iconSize = 48.dp
val iconSizePx = iconSize.toPixels()
val suggestions by
remember { viewModel.getIconSuggestions(iconSizePx.toInt()) }
val scope = rememberCoroutineScope()
val suggestions by remember { viewModel.getIconSuggestions(iconSizePx.toInt()) }
.observeAsState(emptyList())
val iconPackIcons by remember {
viewModel.getAllIconsFromAllIconPacks()
}.observeAsState(emptyList())
val defaultIcon by remember {
viewModel.getDefaultIcon(iconSizePx.toInt())
}.observeAsState()
val pagingItems = iconPackIcons.map {
it.flow.collectAsLazyPagingItems()
}
var query by remember { mutableStateOf("") }
val isSearching by viewModel.isSearchingIcons.observeAsState(initial = false)
val iconResults by viewModel.iconSearchResults.observeAsState(emptyList())
LazyVerticalGrid(columns = GridCells.Fixed(LocalGridColumns.current)) {
items(suggestions) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.padding(vertical = 8.dp)
) {
ShapedLauncherIcon(
size = iconSize,
icon = it.preview,
onClick = {
viewModel.pickIcon(it.customIcon)
val columns = LocalGridColumns.current
LazyVerticalGrid(
modifier = Modifier.fillMaxSize(),
columns = GridCells.Fixed(columns)
) {
item(span = { GridItemSpan(columns) }) {
OutlinedTextField(
modifier = Modifier.padding(bottom = 16.dp),
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = {
query = ""
scope.launch {
viewModel.searchIcon("")
}
}) {
Icon(
imageVector = Icons.Rounded.Clear,
contentDescription = null
)
}
}
},
value = query,
onValueChange = {
query = it
scope.launch {
viewModel.searchIcon(query)
}
},
label = {
Text(stringResource(R.string.icon_picker_search_icon))
}
)
}
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) }
)
}
}
for (pager in pagingItems) {
itemsIndexed(pager) { index, item ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.padding(vertical = 8.dp)
) {
ShapedLauncherIcon(
size = iconSize,
icon = item?.preview,
onClick = {
viewModel.pickIcon(item?.customIcon)
}
)
} else {
item(span = { GridItemSpan(columns) }) {
Separator(stringResource(R.string.icon_picker_packs))
}
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)
)
}
}
}
}
}
}
}
}
fun <T : Any> LazyGridScope.itemsIndexed(
items: LazyPagingItems<T>,
key: ((index: Int, item: T) -> Any)? = null,
itemContent: @Composable LazyGridScope.(index: Int, value: T?) -> Unit
@Composable
fun IconPreview(
item: CustomIconWithPreview?,
iconSize: Dp,
onClick: () -> Unit,
) {
items(
count = items.itemCount,
key = if (key == null) null else { index ->
val item = items.peek(index)
if (item == null) {
} else {
key(index, item)
}
}
) { index ->
this@itemsIndexed.itemContent(index, items[index])
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.padding(vertical = 8.dp)
) {
ShapedLauncherIcon(
size = iconSize,
icon = item?.preview,
onClick = onClick
)
}
}
@Composable
fun Separator(label: String) {
Text(
label,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.padding(top = 16.dp, bottom = 8.dp)
.fillMaxWidth()
)
}

View File

@ -2,15 +2,16 @@ package de.mm20.launcher2.ui.launcher.search.common.customattrs
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.liveData
import androidx.paging.Pager
import androidx.paging.PagingConfig
import de.mm20.launcher2.customattrs.CustomIcon
import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.coroutineContext
class CustomizeSearchableSheetVM(
private val searchable: Searchable
@ -40,19 +41,28 @@ class CustomizeSearchableSheetVM(
closeIconPicker()
}
fun getAllIconsFromAllIconPacks() = liveData {
emit(emptyList())
val iconPacks = iconRepository.getInstalledIconPacks()
fun getDefaultIcon(size: Int) = liveData {
emit(iconRepository.getUncustomizedDefaultIcon(searchable, size))
}
emit(iconPacks.map {
val source = iconRepository.getAllIconsFromPack(it.packageName)
val iconSearchResults = MutableLiveData(emptyList<CustomIconWithPreview>())
val isSearchingIcons = MutableLiveData(false)
Pager(
PagingConfig(pageSize = 20, enablePlaceholders = false, maxSize = 200),
) {
source
private var debounceSearchJob: Job? = null
suspend fun searchIcon(query: String) {
debounceSearchJob?.cancelAndJoin()
if (query.isBlank()) {
iconSearchResults.value = emptyList()
isSearchingIcons.value = false
return
}
withContext(coroutineContext) {
debounceSearchJob = launch {
delay(1000)
isSearchingIcons.value = true
iconSearchResults.value = iconRepository.searchIconPackIcon(query)
isSearchingIcons.value = false
}
})
}
}
}