Add themed icons to icon picker, add option to force themed icons
This commit is contained in:
parent
b979c81501
commit
389b33aebe
@ -81,8 +81,7 @@ sealed class CustomIcon : CustomAttribute {
|
||||
}
|
||||
"custom_themed_icon" -> {
|
||||
CustomThemedIcon(
|
||||
iconName = payload.getString("icon"),
|
||||
iconPackPackage = payload.getString("icon_pack")
|
||||
iconPackageName = payload.getString("icon"),
|
||||
)
|
||||
}
|
||||
"default_icon" -> {
|
||||
@ -143,14 +142,12 @@ data class AdaptifiedLegacyIcon(
|
||||
}
|
||||
|
||||
data class CustomThemedIcon(
|
||||
val iconPackPackage: String,
|
||||
val iconName: String,
|
||||
val iconPackageName: String,
|
||||
) : CustomIcon() {
|
||||
override fun toDatabaseValue(): String {
|
||||
return jsonObjectOf(
|
||||
"type" to "custom_themed_icon",
|
||||
"icon" to iconName,
|
||||
"icon_pack" to iconPackPackage,
|
||||
"icon" to iconPackageName,
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,9 +19,12 @@ 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 :query ORDER BY iconPack, drawable LIMIT :limit")
|
||||
@Query("SELECT * FROM Icons WHERE (type = 'app' OR type = 'calendar') AND (drawable LIKE :query OR componentName LIKE :query) ORDER BY iconPack, drawable LIMIT :limit")
|
||||
suspend fun searchIconPackIcons(query: 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>
|
||||
|
||||
@Query("DELETE FROM Icons WHERE iconPack = :iconPack")
|
||||
fun deleteIcons(iconPack: String)
|
||||
|
||||
|
||||
@ -454,6 +454,8 @@
|
||||
<string name="preference_theme_system">Follow system</string>
|
||||
<string name="preference_themed_icons">Themed icons</string>
|
||||
<string name="preference_themed_icons_summary">Color icons with the application\'s color scheme</string>
|
||||
<string name="preference_force_themed_icons">Force themed icons</string>
|
||||
<string name="preference_force_themed_icons_summary">Apply the application\'s color scheme to all icons, including unsupported ones (not recommended)</string>
|
||||
<string name="preference_icon_pack">Icon pack</string>
|
||||
<string name="preference_icon_pack_summary_empty">No icon packs installed</string>
|
||||
<string name="preference_category_system_bars">System bars</string>
|
||||
|
||||
@ -8,15 +8,14 @@ import android.content.pm.ResolveInfo
|
||||
import android.content.res.Resources
|
||||
import android.content.res.XmlResourceParser
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.*
|
||||
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.obtainTypedArrayOrNull
|
||||
import de.mm20.launcher2.ktx.randomElementOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -237,6 +236,138 @@ class IconPackManager(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getThemedIcon(packageName: String): LauncherIcon? {
|
||||
val icon = getGreyscaleIcon(packageName) ?: return null
|
||||
val resId = icon.drawable?.toIntOrNull() ?: return null
|
||||
try {
|
||||
val resources = context.packageManager.getResourcesForApplication(icon.iconPack)
|
||||
return getThemedClockIcon(resources, resId) ?: getThemedCalendarIcon(
|
||||
resources,
|
||||
resId,
|
||||
iconProviderPackage = icon.iconPack
|
||||
) ?: getThemedStaticIcon(resources, resId)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
suspend fun getGreyscaleIcon(packageName: String): IconPackIcon? {
|
||||
val iconDao = AppDatabase.getInstance(context).iconDao()
|
||||
return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString())
|
||||
?.let { IconPackIcon(it) }
|
||||
|
||||
}
|
||||
|
||||
private fun getThemedStaticIcon(resources: Resources, resId: Int): LauncherIcon? {
|
||||
try {
|
||||
val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedIconLayer(
|
||||
icon = fg,
|
||||
scale = 0.5f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThemedClockIcon(resources: Resources, resId: Int): LauncherIcon? {
|
||||
try {
|
||||
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
|
||||
var i = 0
|
||||
var drawable: LayerDrawable? = null
|
||||
var minuteIndex: Int? = null
|
||||
var defaultMinute = 0
|
||||
var hourIndex: Int? = null
|
||||
var defaultHour = 0
|
||||
while (i < array.length()) {
|
||||
when (array.getString(i)) {
|
||||
"com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> {
|
||||
i++
|
||||
drawable = array.getDrawable(i) as? LayerDrawable
|
||||
}
|
||||
"com.android.launcher3.HOUR_LAYER_INDEX" -> {
|
||||
i++
|
||||
hourIndex = array.getInt(i, -1).takeIf { it != -1 }
|
||||
}
|
||||
"com.android.launcher3.MINUTE_LAYER_INDEX" -> {
|
||||
i++
|
||||
minuteIndex = array.getInt(i, -1).takeIf { it != -1 }
|
||||
}
|
||||
"com.android.launcher3.DEFAULT_HOUR" -> {
|
||||
i++
|
||||
defaultHour = array.getInt(i, 0)
|
||||
}
|
||||
"com.android.launcher3.DEFAULT_MINUTE" -> {
|
||||
i++
|
||||
defaultMinute = array.getInt(i, 0)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (drawable != null && minuteIndex != null && hourIndex != null) {
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedClockLayer(
|
||||
sublayers = (0 until drawable.numberOfLayers).map {
|
||||
val drw = drawable.getDrawable(it)
|
||||
if (drw is RotateDrawable) {
|
||||
drw.level = when (it) {
|
||||
hourIndex -> {
|
||||
(12 - defaultHour) * 60
|
||||
}
|
||||
minuteIndex -> {
|
||||
(60 - defaultMinute)
|
||||
}
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
ClockSublayer(
|
||||
drawable = drw,
|
||||
role = when (it) {
|
||||
hourIndex -> ClockSublayerRole.Hour
|
||||
minuteIndex -> ClockSublayerRole.Minute
|
||||
else -> ClockSublayerRole.Static
|
||||
}
|
||||
)
|
||||
},
|
||||
scale = 1.5f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
}
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getThemedCalendarIcon(
|
||||
resources: Resources,
|
||||
resId: Int,
|
||||
iconProviderPackage: String
|
||||
): LauncherIcon? {
|
||||
try {
|
||||
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
|
||||
if (array.length() != 31) return null
|
||||
|
||||
return DynamicCalendarIcon(
|
||||
resources = resources,
|
||||
resourceIds = IntArray(31) {
|
||||
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
|
||||
},
|
||||
isThemed = true
|
||||
)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun searchIconPackIcon(query: String): List<IconPackIcon> {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
return iconDao.searchIconPackIcons("%$query%").map {
|
||||
@ -244,6 +375,12 @@ class IconPackManager(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchThemedIcons(query: String): List<IconPackIcon> {
|
||||
val iconDao = appDatabase.iconDao()
|
||||
return iconDao.searchGreyscaleIcons("%$query%").map {
|
||||
IconPackIcon(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import android.graphics.Color
|
||||
import android.util.LruCache
|
||||
import de.mm20.launcher2.customattrs.*
|
||||
import de.mm20.launcher2.icons.providers.*
|
||||
import de.mm20.launcher2.icons.transformations.ForceThemedIconTransformation
|
||||
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
|
||||
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
|
||||
import de.mm20.launcher2.icons.transformations.transform
|
||||
@ -66,7 +67,7 @@ class IconRepository(
|
||||
val providers = mutableListOf<IconProvider>()
|
||||
|
||||
if (settings.themedIcons) {
|
||||
providers.add(ThemedIconProvider(context))
|
||||
providers.add(ThemedIconProvider(iconPackManager))
|
||||
}
|
||||
|
||||
if (settings.iconPack.isNotBlank()) {
|
||||
@ -87,6 +88,7 @@ class IconRepository(
|
||||
val transformations = mutableListOf<LauncherIconTransformation>()
|
||||
|
||||
if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation())
|
||||
if (settings.themedIcons && settings.forceThemed) transformations.add(ForceThemedIconTransformation())
|
||||
|
||||
this@IconRepository.placeholderProvider = placeholderProvider
|
||||
iconProviders.value = providers
|
||||
@ -140,6 +142,14 @@ class IconRepository(
|
||||
)
|
||||
)
|
||||
}
|
||||
if (customIcon is CustomThemedIcon) {
|
||||
return listOf(
|
||||
CustomThemedIconProvider(
|
||||
customIcon,
|
||||
iconPackManager
|
||||
)
|
||||
)
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@ -243,6 +253,15 @@ class IconRepository(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val themedIcon = iconPackManager.getGreyscaleIcon(searchable.`package`)
|
||||
if (themedIcon != null && themedIcon.componentName?.packageName != null) {
|
||||
providerOptions.add(
|
||||
CustomThemedIcon(
|
||||
iconPackageName = themedIcon.componentName.packageName,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suggestions.addAll(
|
||||
@ -272,9 +291,9 @@ class IconRepository(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun searchIconPackIcon(query: String): List<CustomIconWithPreview> {
|
||||
suspend fun searchCustomIcons(query: String): List<CustomIconWithPreview> {
|
||||
val transformations = this.transformations.first()
|
||||
return iconPackManager.searchIconPackIcon(query).mapNotNull {
|
||||
val iconPackIcons = iconPackManager.searchIconPackIcon(query).mapNotNull {
|
||||
val componentName = it.componentName ?: return@mapNotNull null
|
||||
|
||||
CustomIconWithPreview(
|
||||
@ -285,6 +304,19 @@ class IconRepository(
|
||||
preview = iconPackManager.getIcon(it.iconPack, componentName)?.transform(transformations) ?: return@mapNotNull null
|
||||
)
|
||||
}
|
||||
|
||||
val themedIcons = iconPackManager.searchThemedIcons(query).mapNotNull {
|
||||
val componentName = it.componentName ?: return@mapNotNull null
|
||||
|
||||
CustomIconWithPreview(
|
||||
customIcon = CustomThemedIcon(
|
||||
iconPackageName = componentName.packageName,
|
||||
),
|
||||
preview = iconPackManager.getThemedIcon(componentName.packageName)?.transform(transformations) ?: return@mapNotNull null
|
||||
)
|
||||
}
|
||||
|
||||
return iconPackIcons + themedIcons
|
||||
}
|
||||
|
||||
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) {
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package de.mm20.launcher2.icons.providers
|
||||
|
||||
import de.mm20.launcher2.customattrs.CustomThemedIcon
|
||||
import de.mm20.launcher2.icons.IconPackManager
|
||||
import de.mm20.launcher2.icons.LauncherIcon
|
||||
import de.mm20.launcher2.search.data.Searchable
|
||||
|
||||
class CustomThemedIconProvider(
|
||||
private val customIcon: CustomThemedIcon,
|
||||
private val iconPackManager: IconPackManager,
|
||||
): IconProvider {
|
||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
||||
return iconPackManager.getThemedIcon(customIcon.iconPackageName)
|
||||
}
|
||||
}
|
||||
@ -10,143 +10,16 @@ import androidx.core.content.res.ResourcesCompat
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.icons.*
|
||||
import de.mm20.launcher2.ktx.getDrawableOrNull
|
||||
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
|
||||
import de.mm20.launcher2.search.data.Application
|
||||
import de.mm20.launcher2.search.data.Searchable
|
||||
|
||||
internal class ThemedIconProvider(
|
||||
private val context: Context,
|
||||
private val iconPackManager: IconPackManager,
|
||||
) : IconProvider {
|
||||
|
||||
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
|
||||
if (searchable !is Application) return null
|
||||
val icon = getGreyscaleIcon(searchable.`package`) ?: return null
|
||||
val resId = icon.drawable?.toIntOrNull() ?: return null
|
||||
try {
|
||||
val resources = context.packageManager.getResourcesForApplication(icon.iconPack)
|
||||
return getClockIcon(resources, resId) ?: getCalendarIcon(
|
||||
resources,
|
||||
resId,
|
||||
iconProviderPackage = icon.iconPack
|
||||
) ?: getStaticIcon(resources, resId)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
private suspend fun getGreyscaleIcon(packageName: String): IconPackIcon? {
|
||||
val iconDao = AppDatabase.getInstance(context).iconDao()
|
||||
return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString())
|
||||
?.let { IconPackIcon(it) }
|
||||
|
||||
}
|
||||
|
||||
private fun getStaticIcon(resources: Resources, resId: Int): LauncherIcon? {
|
||||
try {
|
||||
val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedIconLayer(
|
||||
icon = fg,
|
||||
scale = 0.5f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClockIcon(resources: Resources, resId: Int): LauncherIcon? {
|
||||
try {
|
||||
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
|
||||
var i = 0
|
||||
var drawable: LayerDrawable? = null
|
||||
var minuteIndex: Int? = null
|
||||
var defaultMinute = 0
|
||||
var hourIndex: Int? = null
|
||||
var defaultHour = 0
|
||||
while (i < array.length()) {
|
||||
when (array.getString(i)) {
|
||||
"com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> {
|
||||
i++
|
||||
drawable = array.getDrawable(i) as? LayerDrawable
|
||||
}
|
||||
"com.android.launcher3.HOUR_LAYER_INDEX" -> {
|
||||
i++
|
||||
hourIndex = array.getInt(i, -1).takeIf { it != -1 }
|
||||
}
|
||||
"com.android.launcher3.MINUTE_LAYER_INDEX" -> {
|
||||
i++
|
||||
minuteIndex = array.getInt(i, -1).takeIf { it != -1 }
|
||||
}
|
||||
"com.android.launcher3.DEFAULT_HOUR" -> {
|
||||
i++
|
||||
defaultHour = array.getInt(i, 0)
|
||||
}
|
||||
"com.android.launcher3.DEFAULT_MINUTE" -> {
|
||||
i++
|
||||
defaultMinute = array.getInt(i, 0)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (drawable != null && minuteIndex != null && hourIndex != null) {
|
||||
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = TintedClockLayer(
|
||||
sublayers = (0 until drawable.numberOfLayers).map {
|
||||
val drw = drawable.getDrawable(it)
|
||||
if (drw is RotateDrawable) {
|
||||
drw.level = when (it) {
|
||||
hourIndex -> {
|
||||
(12 - defaultHour) * 60
|
||||
}
|
||||
minuteIndex -> {
|
||||
(60 - defaultMinute)
|
||||
}
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
ClockSublayer(
|
||||
drawable = drw,
|
||||
role = when (it) {
|
||||
hourIndex -> ClockSublayerRole.Hour
|
||||
minuteIndex -> ClockSublayerRole.Minute
|
||||
else -> ClockSublayerRole.Static
|
||||
}
|
||||
)
|
||||
},
|
||||
scale = 1.5f,
|
||||
),
|
||||
backgroundLayer = ColorLayer()
|
||||
)
|
||||
}
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getCalendarIcon(
|
||||
resources: Resources,
|
||||
resId: Int,
|
||||
iconProviderPackage: String
|
||||
): LauncherIcon? {
|
||||
try {
|
||||
val array = resources.obtainTypedArrayOrNull(resId) ?: return null
|
||||
if (array.length() != 31) return null
|
||||
|
||||
return DynamicCalendarIcon(
|
||||
resources = resources,
|
||||
resourceIds = IntArray(31) {
|
||||
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
|
||||
},
|
||||
isThemed = true
|
||||
)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
}
|
||||
return null
|
||||
return iconPackManager.getThemedIcon(searchable.`package`)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package de.mm20.launcher2.icons.transformations
|
||||
|
||||
import de.mm20.launcher2.icons.*
|
||||
|
||||
internal class ForceThemedIconTransformation : LauncherIconTransformation {
|
||||
override suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon {
|
||||
return StaticLauncherIcon(
|
||||
foregroundLayer = asThemed(icon.foregroundLayer),
|
||||
backgroundLayer = ColorLayer(0),
|
||||
)
|
||||
}
|
||||
|
||||
private fun asThemed(layer: LauncherIconLayer): LauncherIconLayer {
|
||||
return when(layer) {
|
||||
is ClockLayer -> TintedClockLayer(
|
||||
scale = layer.scale,
|
||||
sublayers = layer.sublayers,
|
||||
)
|
||||
is ColorLayer -> layer.copy(color = 0)
|
||||
is StaticIconLayer -> TintedIconLayer(
|
||||
color = 0,
|
||||
icon = layer.icon,
|
||||
scale = layer.scale / 1.5f,
|
||||
)
|
||||
is TextLayer -> layer.copy(
|
||||
color = 0
|
||||
)
|
||||
else -> layer
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -219,7 +219,9 @@ message Settings {
|
||||
IconShape shape = 1;
|
||||
bool themed_icons = 2;
|
||||
string icon_pack = 3;
|
||||
reserved 4;
|
||||
bool adaptify = 5;
|
||||
bool force_themed = 6;
|
||||
}
|
||||
IconSettings icons = 21;
|
||||
|
||||
|
||||
@ -60,7 +60,7 @@ class CustomizeSearchableSheetVM(
|
||||
debounceSearchJob = launch {
|
||||
delay(1000)
|
||||
isSearchingIcons.value = true
|
||||
iconSearchResults.value = iconRepository.searchIconPackIcon(query)
|
||||
iconSearchResults.value = iconRepository.searchCustomIcons(query)
|
||||
isSearchingIcons.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,6 +199,16 @@ fun AppearanceSettingsScreen() {
|
||||
viewModel.setThemedIcons(it)
|
||||
}
|
||||
)
|
||||
val forceThemedIcons by viewModel.forceThemedIcons.observeAsState()
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.preference_force_themed_icons),
|
||||
summary = stringResource(R.string.preference_force_themed_icons_summary),
|
||||
value = forceThemedIcons == true,
|
||||
enabled = themedIcons == true,
|
||||
onValueChanged = {
|
||||
viewModel.setForceThemedIcons(it)
|
||||
}
|
||||
)
|
||||
|
||||
val iconPack by viewModel.iconPack.observeAsState()
|
||||
val installedIconPacks by viewModel.installedIconPacks.observeAsState(emptyList())
|
||||
|
||||
@ -165,6 +165,20 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
val forceThemedIcons = dataStore.data.map { it.icons.forceThemed }.asLiveData()
|
||||
fun setForceThemedIcons(forceThemedIcons: Boolean) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setIcons(
|
||||
it.icons.toBuilder()
|
||||
.setForceThemed(forceThemedIcons)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val installedIconPacks: LiveData<List<IconPack>> = liveData {
|
||||
emit(
|
||||
listOf(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user