Refactor icons

This commit is contained in:
MM20 2022-06-18 18:51:41 +02:00
parent 08e736f5d1
commit 6de89f3f79
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
54 changed files with 897 additions and 1492 deletions

View File

@ -3,24 +3,22 @@ package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.graphics.Color
import android.graphics.ColorMatrix import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter import android.graphics.ColorMatrixColorFilter
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.mm20.launcher2.applications.R import de.mm20.launcher2.applications.R
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.*
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
class AppInstallation( class AppInstallation(
val session: PackageInstaller.SessionInfo val session: PackageInstaller.SessionInfo
) : Application( ) : Application(
label = session.appLabel?.toString() ?: "", label = session.appLabel?.toString() ?: "",
`package` = session.appPackageName ?: "", `package` = session.appPackageName ?: "",
activity = "", activity = "",
flags = 0, flags = 0,
version = null version = null
) { ) {
override val key: String override val key: String
@ -30,22 +28,31 @@ class AppInstallation(
return session.createDetailsIntent() return session.createDetailsIntent()
} }
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return LauncherIcon( return StaticLauncherIcon(
foreground = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!, foregroundLayer = TintedIconLayer(
background = ColorDrawable(ContextCompat.getColor(context, R.color.grey)), icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
foregroundScale = 0.5f) scale = 0.5f,
color = Color.WHITE
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, R.color.grey))
)
} }
override suspend fun loadIcon(context: Context, size: Int, legacyIconBackground: LegacyIconBackground): LauncherIcon? { override suspend fun loadIcon(
context: Context,
size: Int,
): LauncherIcon {
val icon = session.appIcon ?: return getPlaceholderIcon(context) val icon = session.appIcon ?: return getPlaceholderIcon(context)
val foreground = BitmapDrawable(context.resources, icon) val foreground = BitmapDrawable(context.resources, icon)
foreground.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { foreground.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
setSaturation(0f) setSaturation(0f)
}) })
return LauncherIcon( return StaticLauncherIcon(
foreground = foreground, foregroundLayer = StaticIconLayer(
background = ColorDrawable(ContextCompat.getColor(context, R.color.grey)) icon = foreground,
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, R.color.grey))
) )
} }

View File

@ -4,12 +4,13 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable import android.graphics.Color
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.mm20.launcher2.applications.R import de.mm20.launcher2.applications.R
import de.mm20.launcher2.compat.PackageManagerCompat import de.mm20.launcher2.compat.PackageManagerCompat
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import org.json.JSONObject import org.json.JSONObject
abstract class Application( abstract class Application(
@ -34,11 +35,14 @@ abstract class Application(
return intent return intent
} }
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return LauncherIcon( return StaticLauncherIcon(
foreground = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!, foregroundLayer = TintedIconLayer(
background = ColorDrawable(ContextCompat.getColor(context, R.color.lightgreen)), icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
foregroundScale = 0.5f scale = 0.5f,
color = Color.WHITE,
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, R.color.android_green))
) )
} }
@ -56,7 +60,10 @@ abstract class Application(
get() = "app://$`package`:$activity" get() = "app://$`package`:$activity"
companion object { companion object {
internal fun getStoreLinkForInstaller(installerPackage: String?, packageName: String?): StoreLink? { internal fun getStoreLinkForInstaller(
installerPackage: String?,
packageName: String?
): StoreLink? {
if (packageName == null) return null if (packageName == null) return null
return when (installerPackage) { return when (installerPackage) {
"de.amazon.mShop.android", "com.amazon.venezia" -> { "de.amazon.mShop.android", "com.amazon.venezia" -> {

View File

@ -11,9 +11,8 @@ import android.os.Bundle
import android.os.Process import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -45,7 +44,6 @@ class LauncherApp(
override suspend fun loadIcon( override suspend fun loadIcon(
context: Context, context: Context,
size: Int, size: Int,
legacyIconBackground: LegacyIconBackground
): LauncherIcon? { ): LauncherIcon? {
try { try {
val icon = val icon =
@ -54,17 +52,23 @@ class LauncherApp(
} ?: return null } ?: return null
if (icon is AdaptiveIconDrawable) { if (icon is AdaptiveIconDrawable) {
return LauncherIcon( return StaticLauncherIcon(
foreground = icon.foreground ?: return null, foregroundLayer = StaticIconLayer(
background = icon.background, icon = icon.foreground,
foregroundScale = 1.5f, scale = 1.5f,
backgroundScale = 1.5f ),
backgroundLayer = StaticIconLayer(
icon = icon.background,
scale = 1.5f,
)
) )
} else { } else {
return LauncherIcon( return StaticLauncherIcon(
foreground = icon, foregroundLayer = StaticIconLayer(
foregroundScale = 0.7f, icon = icon,
autoGenerateBackgroundMode = legacyIconBackground.number scale = 1f,
),
backgroundLayer = TransparentLayer
) )
} }
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.graphics.Color
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
@ -11,9 +12,8 @@ import android.os.Process
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.R import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -51,18 +51,20 @@ class AppShortcut(
return true return true
} }
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return LauncherIcon( return StaticLauncherIcon(
foreground = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!, foregroundLayer = TintedIconLayer(
background = ColorDrawable(ContextCompat.getColor(context, R.color.green)), color = Color.WHITE,
foregroundScale = 0.5f icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
scale = 0.5f,
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, R.color.green)),
) )
} }
override suspend fun loadIcon( override suspend fun loadIcon(
context: Context, context: Context,
size: Int, size: Int,
legacyIconBackground: LegacyIconBackground
): LauncherIcon? { ): LauncherIcon? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val icon = withContext(Dispatchers.IO) { val icon = withContext(Dispatchers.IO) {
@ -72,17 +74,23 @@ class AppShortcut(
) )
} ?: return null } ?: return null
if (icon is AdaptiveIconDrawable) { if (icon is AdaptiveIconDrawable) {
return LauncherIcon( return StaticLauncherIcon(
foreground = icon.foreground, foregroundLayer = StaticIconLayer(
background = icon.background, icon = icon.foreground,
foregroundScale = 1.5f, scale = 1.5f,
backgroundScale = 1.5f ),
backgroundLayer = StaticIconLayer(
icon = icon.background,
scale = 1.5f,
)
) )
} }
return LauncherIcon( return StaticLauncherIcon(
foreground = icon, foregroundLayer = StaticIconLayer(
foregroundScale = 1f, icon = icon,
autoGenerateBackgroundMode = legacyIconBackground.number scale = 1f
),
backgroundLayer = TransparentLayer
) )
} }
} }

View File

@ -1,85 +1,14 @@
package de.mm20.launcher2.icons package de.mm20.launcher2.icons
import android.graphics.Color import android.content.res.Resources
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette
import java.lang.ref.WeakReference
open class LauncherIcon( sealed interface LauncherIcon
foreground: Drawable,
background: Drawable? = null,
foregroundScale: Float = 1f,
backgroundScale: Float = 1f,
var autoGenerateBackgroundMode: Int = BACKGROUND_WHITE,
val isThemeable: Boolean = false,
) {
var foreground = foreground data class StaticLauncherIcon(
set(value) { val foregroundLayer: LauncherIconLayer,
field = value val backgroundLayer: LauncherIconLayer,
updateBackgroundColor() ): LauncherIcon
notifyCallbacks()
}
private fun updateBackgroundColor() { interface DynamicLauncherIcon: LauncherIcon {
if (background == null) { suspend fun getIcon(time: Long): StaticLauncherIcon
when (autoGenerateBackgroundMode) {
BACKGROUND_DYNAMIC -> {
val palette = Palette
.from(foreground.toBitmap())
.generate()
this.background = ColorDrawable(palette.getDominantColor(Color.WHITE))
badgeColor = palette.getLightVibrantColor(0xFFF0F0F0.toInt())
}
BACKGROUND_WHITE -> this.background = ColorDrawable(Color.WHITE)
else -> this.foregroundScale = 1f
}
}
}
var background = background
set(value) {
field = value
notifyCallbacks()
}
var foregroundScale = foregroundScale
set(value) {
field = value
notifyCallbacks()
}
var backgroundScale = backgroundScale
set(value) {
field = value
notifyCallbacks()
}
private val callbacks = mutableListOf<WeakReference<(LauncherIcon) -> Unit>>()
fun registerCallback(callback: (LauncherIcon) -> Unit) {
callbacks.add(WeakReference(callback))
}
protected fun notifyCallbacks() {
val iterator = callbacks.iterator()
while(iterator.hasNext()) {
val callback = iterator.next()
callback.get()?.invoke(this) ?: iterator.remove()
}
}
var badgeColor: Int = 0xFFF0F0F0.toInt()
init {
updateBackgroundColor()
}
companion object {
const val BACKGROUND_NONE = 1
const val BACKGROUND_DYNAMIC = 0
const val BACKGROUND_WHITE = 2
}
} }

View File

@ -0,0 +1,50 @@
package de.mm20.launcher2.icons
import android.graphics.drawable.Drawable
sealed interface LauncherIconLayer
data class StaticIconLayer(
val icon: Drawable,
val scale: Float = 1f,
) : LauncherIconLayer
data class ColorLayer(
val color: Int = 0,
) : LauncherIconLayer
data class ClockLayer(
val sublayers: List<ClockSublayer>,
val scale: Float,
) : LauncherIconLayer
data class ClockSublayer(
val drawable: Drawable,
val role: ClockSublayerRole
)
enum class ClockSublayerRole {
Hour,
Minute,
Second,
Static,
}
data class TintedIconLayer(
val icon: Drawable,
val scale: Float = 0.5f,
val color: Int = 0
) : LauncherIconLayer
data class TintedClockLayer(
val sublayers: List<ClockSublayer>,
val scale: Float,
val color: Int = 0,
) : LauncherIconLayer
data class TextLayer(
val text: String,
val color: Int = 0,
) : LauncherIconLayer
object TransparentLayer: LauncherIconLayer

View File

@ -6,23 +6,14 @@ import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable
import android.provider.CalendarContract import android.provider.CalendarContract
import android.text.format.DateFormat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import de.mm20.launcher2.calendar.R
import de.mm20.launcher2.graphics.TextDrawable import de.mm20.launcher2.graphics.TextDrawable
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.ktx.dp import de.mm20.launcher2.ktx.dp
import hct.Hct
import palettes.TonalPalette import palettes.TonalPalette
import scheme.Scheme
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.*
class CalendarEvent( class CalendarEvent(
override val label: String, override val label: String,
@ -41,21 +32,14 @@ class CalendarEvent(
get() = "calendar://$id" get() = "calendar://$id"
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val df = SimpleDateFormat("dd") val df = SimpleDateFormat("dd")
val s = (48 * context.dp).toInt() return StaticLauncherIcon(
val foreground = TextDrawable( foregroundLayer = TextLayer(
df.format(startTime), text = df.format(startTime),
color = Color.WHITE, color = Color.WHITE
fontSize = 24 * context.dp, ),
typeface = Typeface.DEFAULT_BOLD, backgroundLayer = ColorLayer(getDisplayColor())
height = s
)
val background = ColorDrawable(getDisplayColor())
return LauncherIcon(
foreground = foreground,
background = background,
foregroundScale = 0.74f
) )
} }

View File

@ -4,20 +4,14 @@ import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.provider.ContactsContract import android.provider.ContactsContract
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import de.mm20.launcher2.contacts.R import de.mm20.launcher2.contacts.R
import de.mm20.launcher2.graphics.TextDrawable import de.mm20.launcher2.icons.*
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.asBitmap import de.mm20.launcher2.ktx.asBitmap
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.sp
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.URLEncoder import java.net.URLEncoder
@ -44,25 +38,19 @@ class Contact(
return phones.union(emails).joinToString(separator = ", ") { it.label } return phones.union(emails).joinToString(separator = ", ") { it.label }
} }
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val iconText = val iconText =
if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else "" if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
return LauncherIcon(
foreground = TextDrawable( return StaticLauncherIcon(
iconText, foregroundLayer = TextLayer(text = iconText, color = Color.WHITE),
Color.WHITE, backgroundLayer = ColorLayer(ContextCompat.getColor(context, R.color.blue))
fontSize = 20 * context.sp,
height = (48 * context.dp).toInt(),
typeface = Typeface.DEFAULT_BOLD
),
background = ColorDrawable(ContextCompat.getColor(context, R.color.blue))
) )
} }
override suspend fun loadIcon( override suspend fun loadIcon(
context: Context, context: Context,
size: Int, size: Int,
legacyIconBackground: LegacyIconBackground
): LauncherIcon? { ): LauncherIcon? {
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
val bmp = withContext(Dispatchers.IO) { val bmp = withContext(Dispatchers.IO) {
@ -71,10 +59,12 @@ class Contact(
ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false) ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false)
?.asBitmap() ?.asBitmap()
} ?: return null } ?: return null
return LauncherIcon(
foreground = bmp.toDrawable(context.resources), return StaticLauncherIcon(
background = null, foregroundLayer = StaticIconLayer(
autoGenerateBackgroundMode = legacyIconBackground.number icon = bmp.toDrawable(context.resources),
),
backgroundLayer = ColorLayer()
) )
} }
@ -160,7 +150,10 @@ class Contact(
val data3 = dataCursor.getStringOrNull(data3Column) val data3 = dataCursor.getStringOrNull(data3Column)
?: continue@loop ?: continue@loop
telegram.add( telegram.add(
ContactInfo(data3.substringAfterLast(" "), "tg:openmessage?user_id=$data1") ContactInfo(
data3.substringAfterLast(" "),
"tg:openmessage?user_id=$data1"
)
) )
} }
"vnd.android.cursor.item/vnd.com.whatsapp.profile" -> { "vnd.android.cursor.item/vnd.com.whatsapp.profile" -> {

View File

@ -1,10 +1,12 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.graphics.drawable.ColorDrawable import android.graphics.Color
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import java.util.* import java.util.*
abstract class File( abstract class File(
@ -19,7 +21,7 @@ abstract class File(
open val providerIconRes: Int? = null open val providerIconRes: Int? = null
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val (resId, bgColor) = when { val (resId, bgColor) = when {
isDirectory -> R.drawable.ic_file_folder to R.color.lightblue isDirectory -> R.drawable.ic_file_folder to R.color.lightblue
mimeType.startsWith("image/") -> R.drawable.ic_file_picture to R.color.teal mimeType.startsWith("image/") -> R.drawable.ic_file_picture to R.color.teal
@ -47,17 +49,23 @@ abstract class File(
else -> R.drawable.ic_file_generic to R.color.bluegrey else -> R.drawable.ic_file_generic to R.color.bluegrey
} }
} }
return LauncherIcon( return StaticLauncherIcon(
foreground = context.getDrawable(resId)!!, foregroundLayer = TintedIconLayer(
background = ColorDrawable(ContextCompat.getColor(context, bgColor)), icon = ContextCompat.getDrawable(context, resId)!!,
foregroundScale = 0.5f scale = 0.5f,
color = Color.WHITE
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, bgColor))
) )
} }
fun getFileType(context: Context): String { fun getFileType(context: Context): String {
if (isDirectory) return context.getString(R.string.file_type_directory) if (isDirectory) return context.getString(R.string.file_type_directory)
if (mimeType == "application/vendor.de.mm20.launcher2.backup") { if (mimeType == "application/vendor.de.mm20.launcher2.backup") {
return context.getString(R.string.file_type_launcherbackup, context.getString(R.string.app_name)) return context.getString(
R.string.file_type_launcherbackup,
context.getString(R.string.app_name)
)
} }
val resource = when (mimeType) { val resource = when (mimeType) {
"application/zip", "application/zip",
@ -107,7 +115,10 @@ abstract class File(
} }
if (resource == R.string.file_type_none && label.matches(Regex(".+\\..+"))) { if (resource == R.string.file_type_none && label.matches(Regex(".+\\..+"))) {
val extension = label.substringAfterLast(".").toUpperCase(Locale.getDefault()) val extension = label.substringAfterLast(".").toUpperCase(Locale.getDefault())
if (extension == "kvaesitso") return context.getString(R.string.file_type_launcherbackup, context.getString(R.string.app_name)) if (extension == "kvaesitso") return context.getString(
R.string.file_type_launcherbackup,
context.getString(R.string.app_name)
)
return context.getString(R.string.file_type_generic, extension) return context.getString(R.string.file_type_generic, extension)
} }
return context.getString(resource) return context.getString(resource)

View File

@ -15,10 +15,12 @@ import android.util.Size
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.ktx.formatToString import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.media.ThumbnailUtilsCompat import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -43,7 +45,6 @@ open class LocalFile(
override suspend fun loadIcon( override suspend fun loadIcon(
context: Context, context: Context,
size: Int, size: Int,
legacyIconBackground: LegacyIconBackground
): LauncherIcon? { ): LauncherIcon? {
if (!JavaIOFile(path).exists()) return null if (!JavaIOFile(path).exists()) return null
when { when {
@ -55,9 +56,12 @@ open class LocalFile(
) )
} ?: return null } ?: return null
return LauncherIcon( return StaticLauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail), foregroundLayer = StaticIconLayer(
autoGenerateBackgroundMode = legacyIconBackground.number icon = BitmapDrawable(context.resources, thumbnail),
scale = 1f,
),
backgroundLayer = ColorLayer()
) )
} }
mimeType.startsWith("video/") -> { mimeType.startsWith("video/") -> {
@ -67,9 +71,13 @@ open class LocalFile(
Size(size, size) Size(size, size)
) )
} ?: return null } ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail), return StaticLauncherIcon(
autoGenerateBackgroundMode = legacyIconBackground.number foregroundLayer = StaticIconLayer(
icon = BitmapDrawable(context.resources, thumbnail),
scale = 1f,
),
backgroundLayer = ColorLayer()
) )
} }
mimeType.startsWith("audio/") -> { mimeType.startsWith("audio/") -> {
@ -94,9 +102,13 @@ open class LocalFile(
} }
thumbnail ?: return null thumbnail ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail), return StaticLauncherIcon(
autoGenerateBackgroundMode = legacyIconBackground.number foregroundLayer = StaticIconLayer(
icon = BitmapDrawable(context.resources, thumbnail),
scale = 1f,
),
backgroundLayer = ColorLayer()
) )
} }
@ -107,18 +119,24 @@ open class LocalFile(
} ?: return null } ?: return null
when { when {
icon is AdaptiveIconDrawable -> { icon is AdaptiveIconDrawable -> {
return LauncherIcon( return StaticLauncherIcon(
foreground = icon.foreground, foregroundLayer = StaticIconLayer(
background = icon.background, icon = icon.foreground,
foregroundScale = 1.5f, scale = 1.5f,
backgroundScale = 1.5f ),
backgroundLayer = StaticIconLayer(
icon = icon.background,
scale = 1.5f,
)
) )
} }
else -> { else -> {
return LauncherIcon( return StaticLauncherIcon(
foreground = icon, foregroundLayer = StaticIconLayer(
foregroundScale = 0.7f, icon = icon,
autoGenerateBackgroundMode = legacyIconBackground.number scale = 0.7f,
),
backgroundLayer = ColorLayer()
) )
} }
} }

View File

@ -188,8 +188,6 @@
<string name="weather_condition_heavyrainandthunder">Starkregen und Gewitter</string> <string name="weather_condition_heavyrainandthunder">Starkregen und Gewitter</string>
<string name="weather_condition_wind">Wind</string> <string name="weather_condition_wind">Wind</string>
<string name="weather_condition_unknown">Unbekannt</string> <string name="weather_condition_unknown">Unbekannt</string>
<string name="preference_legacy_icon_bg">Symbol-Hintergrund</string>
<string name="preference_legacy_icon_bg_summary">Stil von Legacy-Icons</string>
<string name="easter_egg_1">Hier gibt es keine Easter Eggs, es sei denn Ihr hättet sie mitgebracht.</string> <string name="easter_egg_1">Hier gibt es keine Easter Eggs, es sei denn Ihr hättet sie mitgebracht.</string>
<string name="easter_egg_2">Bitte, hör auf, du verschwendest deine Zeit</string> <string name="easter_egg_2">Bitte, hör auf, du verschwendest deine Zeit</string>
<string name="easter_egg_3">Ich werde es nicht noch einmal sagen: hier sind definitiv keine Easter Eggs versteckt</string> <string name="easter_egg_3">Ich werde es nicht noch einmal sagen: hier sind definitiv keine Easter Eggs versteckt</string>

View File

@ -173,8 +173,6 @@
<string name="shortcut_summary">Par %1$s</string> <string name="shortcut_summary">Par %1$s</string>
<string name="installation_in_progress">Installation en cours… (%1$s)</string> <string name="installation_in_progress">Installation en cours… (%1$s)</string>
<string name="error_activity_not_found">Impossible d\'ouvrir %1$s</string> <string name="error_activity_not_found">Impossible d\'ouvrir %1$s</string>
<string name="preference_legacy_icon_bg">Arrière-plan de l\'icône</string>
<string name="preference_legacy_icon_bg_summary">Style d\'icône legacy</string>
<!-- Easter egg preference toasts --> <!-- Easter egg preference toasts -->
<string name="easter_egg_1">Il n\'y a pas d\'easter egg ici, à moins que vous ne l\'ayez amené avec vous.</string> <string name="easter_egg_1">Il n\'y a pas d\'easter egg ici, à moins que vous ne l\'ayez amené avec vous.</string>
<string name="easter_egg_2">Par pitié, arrêtez, vous perdez votre temps</string> <string name="easter_egg_2">Par pitié, arrêtez, vous perdez votre temps</string>

View File

@ -291,7 +291,6 @@
<string name="preference_owncloud_signin_summary">Accedi per cercare sul tuo server Owncloud</string> <string name="preference_owncloud_signin_summary">Accedi per cercare sul tuo server Owncloud</string>
<string name="preference_about_telegram">Gruppo Telegram</string> <string name="preference_about_telegram">Gruppo Telegram</string>
<string name="preference_about_fdroid">Repository F-Droid</string> <string name="preference_about_fdroid">Repository F-Droid</string>
<string name="preference_legacy_icon_bg">Sfondo icona</string>
<string name="preference_category_license">Licenza</string> <string name="preference_category_license">Licenza</string>
<string name="preference_category_grid">Griglia</string> <string name="preference_category_grid">Griglia</string>
<string name="preference_grid_column_count">Numero di colonne</string> <string name="preference_grid_column_count">Numero di colonne</string>
@ -303,7 +302,6 @@
<string name="preference_screen_musicwidget">Musica</string> <string name="preference_screen_musicwidget">Musica</string>
<string name="preference_screen_clockwidget">Orologio</string> <string name="preference_screen_clockwidget">Orologio</string>
<string name="preference_clockwidget_layout_vertical">Verticale</string> <string name="preference_clockwidget_layout_vertical">Verticale</string>
<string name="preference_legacy_icon_bg_summary">Stile icone legacy</string>
<string name="preference_about_license_summary">Concesso in licenza sotto la GNU General Public License 3.0</string> <string name="preference_about_license_summary">Concesso in licenza sotto la GNU General Public License 3.0</string>
<string name="preference_clock_widget_style">Stile</string> <string name="preference_clock_widget_style">Stile</string>
<string name="preference_clock_widget_style_summary">Seleziona un orologio</string> <string name="preference_clock_widget_style_summary">Seleziona un orologio</string>

View File

@ -196,7 +196,6 @@
<string name="preference_owncloud_signin">Zaloguj się do Owncloud</string> <string name="preference_owncloud_signin">Zaloguj się do Owncloud</string>
<string name="preference_about_telegram">Grupa Telegram</string> <string name="preference_about_telegram">Grupa Telegram</string>
<string name="preference_about_fdroid">Repozytorium F-Droid</string> <string name="preference_about_fdroid">Repozytorium F-Droid</string>
<string name="preference_legacy_icon_bg">Tło ikon</string>
<string name="preference_owncloud_signin_summary">Zaloguj się, aby móc przeszukiwać twój serwer Owncloud</string> <string name="preference_owncloud_signin_summary">Zaloguj się, aby móc przeszukiwać twój serwer Owncloud</string>
<string name="preference_about_license">Ta aplikacja jest wolnym oprogramowaniem.</string> <string name="preference_about_license">Ta aplikacja jest wolnym oprogramowaniem.</string>
<string name="preference_category_grid">Siatka</string> <string name="preference_category_grid">Siatka</string>
@ -212,7 +211,6 @@
<string name="preference_clockwidget_layout">Układ</string> <string name="preference_clockwidget_layout">Układ</string>
<string name="preference_clockwidget_layout_vertical">Pionowy</string> <string name="preference_clockwidget_layout_vertical">Pionowy</string>
<string name="preference_clockwidget_layout_horizontal">Poziomy</string> <string name="preference_clockwidget_layout_horizontal">Poziomy</string>
<string name="preference_legacy_icon_bg_summary">Przestarzały styl ikon</string>
<string name="preference_clock_widget_style_summary">Wybierz typ zegara</string> <string name="preference_clock_widget_style_summary">Wybierz typ zegara</string>
<string name="preference_clockwidget_date_part">Data</string> <string name="preference_clockwidget_date_part">Data</string>
<string name="preference_clockwidget_date_part_summary">Pokaż aktualną datę</string> <string name="preference_clockwidget_date_part_summary">Pokaż aktualną datę</string>

View File

@ -294,7 +294,6 @@
<string name="preference_category_services_owncloud">Owncloud</string> <string name="preference_category_services_owncloud">Owncloud</string>
<string name="preference_owncloud_signin_summary">Autenticar para pesquisar no seu servidor Owncloud</string> <string name="preference_owncloud_signin_summary">Autenticar para pesquisar no seu servidor Owncloud</string>
<string name="preference_about_fdroid">Repositório F-Droid</string> <string name="preference_about_fdroid">Repositório F-Droid</string>
<string name="preference_legacy_icon_bg">Fundo do ícone</string>
<string name="preference_category_license">Licença</string> <string name="preference_category_license">Licença</string>
<string name="preference_about_license">Este aplicativo é um software livre.</string> <string name="preference_about_license">Este aplicativo é um software livre.</string>
<string name="preference_about_license_summary">Licenciado sob a GNU General Public License 3.0</string> <string name="preference_about_license_summary">Licenciado sob a GNU General Public License 3.0</string>

View File

@ -278,8 +278,6 @@
<string name="preference_owncloud_signin_summary">Conectează-te pentru a căuta în serverul tău Owncloud</string> <string name="preference_owncloud_signin_summary">Conectează-te pentru a căuta în serverul tău Owncloud</string>
<string name="preference_about_telegram">Grup Telegram</string> <string name="preference_about_telegram">Grup Telegram</string>
<string name="preference_about_fdroid">Repository F-Droid</string> <string name="preference_about_fdroid">Repository F-Droid</string>
<string name="preference_legacy_icon_bg">Fundal pictogramă</string>
<string name="preference_legacy_icon_bg_summary">Stilul pictogramei clasice</string>
<string name="preference_category_license">Licență</string> <string name="preference_category_license">Licență</string>
<string name="preference_about_license">Această aplicație este un software gratuit.</string> <string name="preference_about_license">Această aplicație este un software gratuit.</string>
<string name="preference_category_grid">Grilă</string> <string name="preference_category_grid">Grilă</string>

View File

@ -260,8 +260,6 @@
<string name="preference_owncloud_signin">Logga in på Owncloud</string> <string name="preference_owncloud_signin">Logga in på Owncloud</string>
<string name="preference_about_telegram">Telegram-grupp</string> <string name="preference_about_telegram">Telegram-grupp</string>
<string name="preference_about_fdroid">F-Droid-kodförråd</string> <string name="preference_about_fdroid">F-Droid-kodförråd</string>
<string name="preference_legacy_icon_bg">Ikonbakgrund</string>
<string name="preference_legacy_icon_bg_summary">Äldre ikonstil</string>
<string name="preference_category_license">Licens</string> <string name="preference_category_license">Licens</string>
<string name="preference_about_license">Denna app är fri programvara.</string> <string name="preference_about_license">Denna app är fri programvara.</string>
<string name="preference_about_license_summary">Licensierad under GNU General Public License 3.0</string> <string name="preference_about_license_summary">Licensierad under GNU General Public License 3.0</string>

View File

@ -294,8 +294,6 @@
<string name="preference_owncloud_signin_summary">登陆以查找你的Owncloud服务</string> <string name="preference_owncloud_signin_summary">登陆以查找你的Owncloud服务</string>
<string name="preference_about_telegram">Telegram群组</string> <string name="preference_about_telegram">Telegram群组</string>
<string name="preference_about_fdroid">F-Droid repository</string> <string name="preference_about_fdroid">F-Droid repository</string>
<string name="preference_legacy_icon_bg">图标背景</string>
<string name="preference_legacy_icon_bg_summary">经典图标风格</string>
<string name="preference_category_license">许可证</string> <string name="preference_category_license">许可证</string>
<string name="preference_about_license_summary">根据 GNU 通用公共许可证 3.0 授权</string> <string name="preference_about_license_summary">根据 GNU 通用公共许可证 3.0 授权</string>
<string name="preference_category_grid">网格</string> <string name="preference_category_grid">网格</string>

View File

@ -491,8 +491,8 @@
<string name="preference_owncloud_signin_summary">Sign in to search your Owncloud server</string> <string name="preference_owncloud_signin_summary">Sign in to search your Owncloud server</string>
<string name="preference_about_telegram">Telegram group</string> <string name="preference_about_telegram">Telegram group</string>
<string name="preference_about_fdroid">F-Droid repository</string> <string name="preference_about_fdroid">F-Droid repository</string>
<string name="preference_legacy_icon_bg">Icon background</string> <string name="preference_enforce_icon_shape">Enforce shape</string>
<string name="preference_legacy_icon_bg_summary">Legacy icon style</string> <string name="preference_enforce_icon_shape_summary">Apply shape to all icons including those that would normally not support it</string>
<string name="preference_category_license">License</string> <string name="preference_category_license">License</string>
<string name="preference_about_license">This app is free software.</string> <string name="preference_about_license">This app is free software.</string>
<string name="preference_about_license_summary">Licensed under the GNU General Public License 3.0</string> <string name="preference_about_license_summary">Licensed under the GNU General Public License 3.0</string>

View File

@ -1,53 +0,0 @@
package de.mm20.launcher2.icons
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import de.mm20.launcher2.ktx.getDrawableOrNull
import java.util.*
import java.util.concurrent.Executors
class CalendarDynamicLauncherIcon(
foreground: Drawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
val packageName: String,
val drawableIds: IntArray,
) : DynamicLauncherIcon(
foreground,
background,
foregroundScale,
backgroundScale,
) {
var currentDay = 0
override fun update(context: Context) {
val calendar = Calendar.getInstance()
val day = calendar[Calendar.DAY_OF_MONTH]
if (day == currentDay || drawableIds.size < currentDay) return
val resources = try {
context.packageManager.getResourcesForApplication(packageName)
} catch (e: PackageManager.NameNotFoundException) {
return
}
Executors.newSingleThreadExecutor().execute {
val currentDayDrawable = resources.getDrawableOrNull(drawableIds[day - 1])
?: return@execute
if (currentDayDrawable is AdaptiveIconDrawable) {
foreground = currentDayDrawable.foreground
background = currentDayDrawable.background
foregroundScale = 1.5f
backgroundScale = 1.5f
} else {
foregroundScale = 1f
backgroundScale = 1f
background = null
foreground = currentDayDrawable
}
}
currentDay = day
}
}

View File

@ -1,53 +0,0 @@
package de.mm20.launcher2.icons
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RotateDrawable
import android.os.Build
import androidx.annotation.RequiresApi
import java.util.*
import kotlin.math.roundToInt
class ClockDynamicLauncherIcon(
foreground: LayerDrawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
isThemeable: Boolean = false,
val hourLayer: Int,
val minuteLayer: Int,
val secondLayer: Int
) : DynamicLauncherIcon(
foreground,
background,
foregroundScale,
backgroundScale,
isThemeable = isThemeable
) {
init {
foreground.also {
try {
it.setDrawable(secondLayer, ColorDrawable(0))
} catch (e: IndexOutOfBoundsException) {}
(it.getDrawable(hourLayer) as? RotateDrawable)?.fromDegrees = 0f
(it.getDrawable(hourLayer) as? RotateDrawable)?.toDegrees = 360f
(it.getDrawable(minuteLayer) as? RotateDrawable)?.fromDegrees = 0f
(it.getDrawable(minuteLayer) as? RotateDrawable)?.toDegrees = 360f
}
}
override fun update(context: Context) {
val calendar = Calendar.getInstance()
val hourDegrees = calendar[Calendar.HOUR] / 12f * 10000 + calendar[Calendar.MINUTE] / 60f * 10000f / 12
val minuteDegrees = calendar[Calendar.MINUTE] / 60f * 10000
(foreground as LayerDrawable).also {
(it.getDrawable(hourLayer) as? RotateDrawable)?.level = hourDegrees.roundToInt()
(it.getDrawable(minuteLayer) as? RotateDrawable)?.level = minuteDegrees.roundToInt()
}
notifyCallbacks()
}
}

View File

@ -0,0 +1,66 @@
package de.mm20.launcher2.icons
import android.content.res.Resources
import android.graphics.drawable.AdaptiveIconDrawable
import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import java.time.Instant
import java.time.ZoneId
internal class DynamicCalendarIcon(
val resources: Resources,
val resourceIds: IntArray,
val isThemed: Boolean = false,
private val transformations: List<LauncherIconTransformation> = emptyList(),
) : DynamicLauncherIcon {
init {
if (resourceIds.size < 31) throw IllegalArgumentException("DynamicCalendarIcon resourceIds must at least have 31 items")
}
override suspend fun getIcon(time: Long): StaticLauncherIcon {
val day = Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).dayOfMonth
val resId = resourceIds[day - 1]
val drawable = try {
ResourcesCompat.getDrawable(resources, resId, null)
} catch (e: Resources.NotFoundException) {
null
} ?: return StaticLauncherIcon(
foregroundLayer = TextLayer(day.toString()),
backgroundLayer = ColorLayer()
)
var icon = if (isThemed) {
StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = drawable,
scale = 0.5f,
),
backgroundLayer = ColorLayer()
)
} else if (drawable is AdaptiveIconDrawable) {
StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = drawable.foreground,
scale = 1.5f
),
backgroundLayer = StaticIconLayer(
icon = drawable.background,
scale = 1.5f
)
)
} else StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = drawable,
scale = 1f,
),
backgroundLayer = TransparentLayer
)
for (transformation in transformations) {
icon = transformation.transform(icon)
}
return icon
}
}

View File

@ -1,58 +0,0 @@
package de.mm20.launcher2.icons
import android.app.Activity
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import java.lang.ref.WeakReference
class DynamicIconController(val context: Context): LifecycleObserver {
private var timeReceiver: BroadcastReceiver? = null
private val registeredIcons = mutableListOf<WeakReference<DynamicLauncherIcon>>()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun resume() {
timeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
updateAllIcons(context)
}
}
val filter = IntentFilter(Intent.ACTION_TIME_TICK).also {
it.addAction(Intent.ACTION_TIME_CHANGED)
it.addAction(Intent.ACTION_TIMEZONE_CHANGED)
}
context.registerReceiver(timeReceiver, filter)
updateAllIcons(context)
}
private fun updateAllIcons(context: Context) {
val iterator = registeredIcons.iterator()
while (iterator.hasNext()) {
val iconRef = iterator.next()
if (iconRef.get() != null) iconRef.get()?.update(context)
else iterator.remove()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun pause() {
try {
context.unregisterReceiver(timeReceiver)
} catch (e: IllegalArgumentException) {
}
timeReceiver = null
}
fun registerIcon(icon: DynamicLauncherIcon) {
icon.update(context)
registeredIcons.add(WeakReference(icon))
}
}

View File

@ -1,21 +0,0 @@
package de.mm20.launcher2.icons
import android.content.Context
import android.graphics.drawable.Drawable
abstract class DynamicLauncherIcon(
foreground: Drawable,
background: Drawable?,
foregroundScale: Float,
backgroundScale: Float,
isThemeable: Boolean = false
) : LauncherIcon(
foreground,
background,
foregroundScale,
backgroundScale,
isThemeable = isThemeable
) {
abstract fun update(context: Context)
}

View File

@ -3,7 +3,7 @@ package de.mm20.launcher2.icons
import android.content.ComponentName import android.content.ComponentName
import de.mm20.launcher2.database.entities.IconEntity import de.mm20.launcher2.database.entities.IconEntity
data class Icon( data class IconPackIcon(
val type: String, val type: String,
val componentName: ComponentName?, val componentName: ComponentName?,
val drawable: String?, val drawable: String?,

View File

@ -124,7 +124,7 @@ class UpdateIconPacksWorker(val context: Context) {
parser.setInput(inStream) parser.setInput(inStream)
} }
val icons = mutableListOf<Icon>() val icons = mutableListOf<IconPackIcon>()
val iconDao = AppDatabase.getInstance(context).iconDao() val iconDao = AppDatabase.getInstance(context).iconDao()
loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) { loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) {
@ -143,7 +143,7 @@ class UpdateIconPacksWorker(val context: Context) {
) )
) )
?: continue@loop ?: continue@loop
val icon = Icon( val icon = IconPackIcon(
componentName = componentName, componentName = componentName,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
@ -164,7 +164,7 @@ class UpdateIconPacksWorker(val context: Context) {
) )
?: continue@loop ?: continue@loop
val icon = Icon( val icon = IconPackIcon(
componentName = componentName, componentName = componentName,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
@ -176,7 +176,7 @@ class UpdateIconPacksWorker(val context: Context) {
for (i in 0 until parser.attributeCount) { for (i in 0 until parser.attributeCount) {
if (parser.getAttributeName(i).startsWith("img")) { if (parser.getAttributeName(i).startsWith("img")) {
val drawable = parser.getAttributeValue(i) val drawable = parser.getAttributeValue(i)
val icon = Icon( val icon = IconPackIcon(
componentName = null, componentName = null,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
@ -190,7 +190,7 @@ class UpdateIconPacksWorker(val context: Context) {
for (i in 0 until parser.attributeCount) { for (i in 0 until parser.attributeCount) {
if (parser.getAttributeName(i).startsWith("img")) { if (parser.getAttributeName(i).startsWith("img")) {
val drawable = parser.getAttributeValue(i) val drawable = parser.getAttributeValue(i)
val icon = Icon( val icon = IconPackIcon(
componentName = null, componentName = null,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
@ -204,7 +204,7 @@ class UpdateIconPacksWorker(val context: Context) {
for (i in 0 until parser.attributeCount) { for (i in 0 until parser.attributeCount) {
if (parser.getAttributeName(i).startsWith("img")) { if (parser.getAttributeName(i).startsWith("img")) {
val drawable = parser.getAttributeValue(i) val drawable = parser.getAttributeValue(i)
val icon = Icon( val icon = IconPackIcon(
componentName = null, componentName = null,
drawable = drawable, drawable = drawable,
iconPack = pkgName, iconPack = pkgName,
@ -246,7 +246,7 @@ class UpdateIconPacksWorker(val context: Context) {
iconDao.deleteIcons(packageName) iconDao.deleteIcons(packageName)
return return
} }
val icons = mutableListOf<Icon>() val icons = mutableListOf<IconPackIcon>()
val parser = resources.getXml(resId) val parser = resources.getXml(resId)
loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) { loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.eventType != XmlPullParser.START_TAG) continue if (parser.eventType != XmlPullParser.START_TAG) continue
@ -256,7 +256,7 @@ class UpdateIconPacksWorker(val context: Context) {
parser.getAttributeResourceValue(null, "drawable", 0).toString() parser.getAttributeResourceValue(null, "drawable", 0).toString()
val pkg = parser.getAttributeValue(null, "package") val pkg = parser.getAttributeValue(null, "package")
val componentName = ComponentName(pkg, pkg) val componentName = ComponentName(pkg, pkg)
val icon = Icon( val icon = IconPackIcon(
drawable = drawable, drawable = drawable,
componentName = componentName, componentName = componentName,
iconPack = packageName, iconPack = packageName,

View File

@ -6,6 +6,8 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.util.LruCache import android.util.LruCache
import de.mm20.launcher2.icons.providers.* import de.mm20.launcher2.icons.providers.*
import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -17,7 +19,6 @@ import kotlinx.coroutines.launch
class IconRepository( class IconRepository(
val context: Context, val context: Context,
private val iconPackManager: IconPackManager, private val iconPackManager: IconPackManager,
private val dynamicIconController: DynamicIconController,
private val dataStore: LauncherDataStore private val dataStore: LauncherDataStore
) { ) {
@ -34,6 +35,11 @@ class IconRepository(
private var iconProviders: MutableStateFlow<List<IconProvider>> = MutableStateFlow(listOf()) private var iconProviders: MutableStateFlow<List<IconProvider>> = MutableStateFlow(listOf())
private var placeholderProvider: IconProvider? = null private var placeholderProvider: IconProvider? = null
private var transformations: MutableStateFlow<List<LauncherIconTransformation>> =
MutableStateFlow(
listOf()
)
init { init {
requestIconPackListUpdate() requestIconPackListUpdate()
context.registerReceiver(appReceiver, IntentFilter().apply { context.registerReceiver(appReceiver, IntentFilter().apply {
@ -62,19 +68,23 @@ class IconRepository(
providers.add( providers.add(
IconPackIconProvider( IconPackIconProvider(
context, context,
settings.iconPack, settings.iconPack
settings.legacyIconBg
) )
) )
} }
providers.add(GoogleClockIconProvider(context)) providers.add(GoogleClockIconProvider(context))
providers.add(CalendarIconProvider(context)) providers.add(CalendarIconProvider(context))
providers.add(SystemIconProvider(context, settings.legacyIconBg)) providers.add(SystemIconProvider(context))
providers.add(placeholderProvider) providers.add(placeholderProvider)
cache.evictAll() cache.evictAll()
val transformations = mutableListOf<LauncherIconTransformation>()
if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation())
this@IconRepository.placeholderProvider = placeholderProvider this@IconRepository.placeholderProvider = placeholderProvider
iconProviders.value = providers iconProviders.value = providers
this@IconRepository.transformations.value = transformations
} }
} }
} }
@ -82,28 +92,33 @@ class IconRepository(
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = channelFlow { fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = channelFlow {
iconProviders.collectLatest { providers -> iconProviders.collectLatest { providers ->
var icon = cache.get(searchable.key) transformations.collectLatest { transformations ->
if (icon != null) { var icon = cache.get(searchable.key)
send(icon) if (icon != null) {
return@collectLatest send(icon)
} return@collectLatest
}
val placeholder = placeholderProvider?.getIcon(searchable, size)
placeholder?.let { send(it) } val placeholder = placeholderProvider?.getIcon(searchable, size)
placeholder?.let { send(it) }
for (provider in providers) {
val ic = provider.getIcon(searchable, size) for (provider in providers) {
if (ic != null) { val ic = provider.getIcon(searchable, size)
if (ic is DynamicLauncherIcon) { if (ic != null) {
dynamicIconController.registerIcon(ic) icon = ic
} break
icon = ic }
break }
if (icon != null) {
if (icon is StaticLauncherIcon) {
for (transformation in transformations) {
icon = transformation.transform(icon as StaticLauncherIcon)
}
}
cache.put(searchable.key, icon)
send(icon)
} }
}
if (icon != null) {
cache.put(searchable.key, icon)
send(icon)
} }
} }
} }

View File

@ -4,7 +4,6 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val iconsModule = module { val iconsModule = module {
single { DynamicIconController(androidContext()) }
single { IconPackManager(androidContext()) } single { IconPackManager(androidContext()) }
single { IconRepository(androidContext(), get(), get(), get()) } single { IconRepository(androidContext(), get(), get()) }
} }

View File

@ -1,44 +0,0 @@
package de.mm20.launcher2.icons
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import de.mm20.launcher2.ktx.getDrawableOrNull
import java.util.*
import java.util.concurrent.Executors
class ThemedCalendarDynamicLauncherIcon(
foregroundScale: Float,
val packageName: String,
val foregroundIds: IntArray,
background: Drawable,
) : DynamicLauncherIcon(
foreground = ColorDrawable(0),
background = background,
foregroundScale = foregroundScale,
backgroundScale = 1f,
isThemeable = true,
) {
var currentDay = 0
override fun update(context: Context) {
val calendar = Calendar.getInstance()
val day = calendar[Calendar.DAY_OF_MONTH]
if (day == currentDay || foregroundIds.size < currentDay) return
val resources = try {
context.packageManager.getResourcesForApplication(packageName)
} catch (e: PackageManager.NameNotFoundException) {
return
}
Executors.newSingleThreadExecutor().execute {
val currentDayDrawable = resources.getDrawableOrNull(foregroundIds[day - 1])
?: return@execute
foreground = currentDayDrawable
}
currentDay = day
}
}

View File

@ -4,7 +4,7 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import de.mm20.launcher2.icons.CalendarDynamicLauncherIcon import de.mm20.launcher2.icons.DynamicCalendarIcon
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
@ -35,13 +35,9 @@ class CalendarIconProvider(val context: Context): IconProvider {
drawableIds[i] = typedArray.getResourceId(i, 0) drawableIds[i] = typedArray.getResourceId(i, 0)
} }
typedArray.recycle() typedArray.recycle()
return CalendarDynamicLauncherIcon( return DynamicCalendarIcon(
foreground = ColorDrawable(0), resources = resources,
background = ColorDrawable(0), resourceIds = drawableIds
foregroundScale = 1.5f,
backgroundScale = 1.5f,
packageName = component.packageName,
drawableIds = drawableIds
) )
} }
} }

View File

@ -5,12 +5,12 @@ import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.os.Build import android.graphics.drawable.RotateDrawable
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.icons.ClockDynamicLauncherIcon import de.mm20.launcher2.icons.*
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
import kotlin.math.roundToInt
class GoogleClockIconProvider(val context: Context) : IconProvider { class GoogleClockIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? { override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
@ -26,7 +26,7 @@ class GoogleClockIconProvider(val context: Context) : IconProvider {
return null return null
} }
val drawable = val drawable =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.LEVEL_PER_TICK_ICON_ROUND") appInfo.metaData.getInt("com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND")
val resources = pm.getResourcesForApplication(appInfo) val resources = pm.getResourcesForApplication(appInfo)
val baseIcon = try { val baseIcon = try {
ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable
@ -36,19 +36,53 @@ class GoogleClockIconProvider(val context: Context) : IconProvider {
} }
val foreground = baseIcon.foreground as? LayerDrawable ?: return null val foreground = baseIcon.foreground as? LayerDrawable ?: return null
val hourLayer = val hourLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX") appInfo.metaData.getInt("com.android.launcher3.HOUR_LAYER_INDEX")
val minuteLayer = val minuteLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX") appInfo.metaData.getInt("com.android.launcher3.MINUTE_LAYER_INDEX")
val secondLayer = val secondLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX") appInfo.metaData.getInt("com.android.launcher3.SECOND_LAYER_INDEX")
return ClockDynamicLauncherIcon(
foreground = foreground, val defaultHour =
background = baseIcon.background, appInfo.metaData.getInt("com.android.launcher3.DEFAULT_HOUR")
foregroundScale = 1.5f, val defaultMinute =
backgroundScale = 1.5f, appInfo.metaData.getInt("com.android.launcher3.DEFAULT_MINUTE")
hourLayer = hourLayer, val defaultSecond =
minuteLayer = minuteLayer, appInfo.metaData.getInt("com.android.launcher3.DEFAULT_SECOND")
secondLayer = secondLayer
return StaticLauncherIcon(
foregroundLayer = ClockLayer(
sublayers = (0 until foreground.numberOfLayers).map {
val drw = foreground.getDrawable(it)
if (drw is RotateDrawable) {
drw.level = when (it) {
hourLayer -> {
(12 - defaultHour) * 60
}
minuteLayer -> {
(60 - defaultMinute)
}
secondLayer -> {
(60 - defaultSecond) * 10
}
else -> 0
}
}
ClockSublayer(
drawable = drw,
role = when {
it == hourLayer -> ClockSublayerRole.Hour
it == minuteLayer -> ClockSublayerRole.Minute
it == secondLayer -> ClockSublayerRole.Second
else -> ClockSublayerRole.Static
}
)
},
scale = 1.5f,
),
backgroundLayer = StaticIconLayer(
icon = baseIcon.background,
scale = 1.5f,
)
) )
} }
} }

View File

@ -9,13 +9,11 @@ import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.CalendarDynamicLauncherIcon import de.mm20.launcher2.icons.*
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.randomElementOrNull import de.mm20.launcher2.ktx.randomElementOrNull
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
@ -24,9 +22,8 @@ import kotlin.math.roundToInt
class IconPackIconProvider( class IconPackIconProvider(
private val context: Context, private val context: Context,
private val iconPack: String, private val iconPack: String
private val legacyIconBackground: Settings.IconSettings.LegacyIconBackground ): IconProvider {
): IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? { override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
if (searchable !is LauncherApp) return null if (searchable !is LauncherApp) return null
val res = try { val res = try {
@ -49,25 +46,31 @@ class IconPackIconProvider(
val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null
return when { return when {
drawable is AdaptiveIconDrawable -> { drawable is AdaptiveIconDrawable -> {
LauncherIcon( StaticLauncherIcon(
foreground = drawable.foreground, foregroundLayer = StaticIconLayer(
background = drawable.background, icon = drawable.foreground,
foregroundScale = 1.5f, scale = 1.5f
backgroundScale = 1.5f ),
backgroundLayer = StaticIconLayer(
icon = drawable.background,
scale = 1.5f
)
) )
} }
else -> { else -> {
LauncherIcon( StaticLauncherIcon(
foreground = drawable, foregroundLayer = StaticIconLayer(
foregroundScale = getScale(), icon = drawable,
autoGenerateBackgroundMode = legacyIconBackground.number scale = getScale()
),
backgroundLayer = TransparentLayer
) )
} }
} }
} }
private fun getScale(): Float { private fun getScale(): Float {
return 0.7f return 1f
} }
private suspend fun generateIcon( private suspend fun generateIcon(
@ -147,10 +150,12 @@ class IconPackIconProvider(
} }
} }
return LauncherIcon( return StaticLauncherIcon(
foreground = BitmapDrawable(context.resources, bitmap), foregroundLayer = StaticIconLayer(
foregroundScale = getScale(), icon = BitmapDrawable(context.resources, bitmap),
autoGenerateBackgroundMode = legacyIconBackground.number scale = getScale(),
),
backgroundLayer = TransparentLayer
) )
} }
@ -180,7 +185,7 @@ class IconPackIconProvider(
private fun getIconPackCalendarIcon( private fun getIconPackCalendarIcon(
context: Context, context: Context,
baseIconName: String baseIconName: String
): CalendarDynamicLauncherIcon? { ): DynamicCalendarIcon? {
val resources = try { val resources = try {
context.packageManager.getResourcesForApplication(iconPack) context.packageManager.getResourcesForApplication(iconPack)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
@ -192,13 +197,9 @@ class IconPackIconProvider(
if (id == 0) return null if (id == 0) return null
id id
}.toIntArray() }.toIntArray()
return CalendarDynamicLauncherIcon( return DynamicCalendarIcon(
foreground = ColorDrawable(0), resources = resources,
background = ColorDrawable(0), resourceIds = drawableIds
foregroundScale = 1.5f,
backgroundScale = 1.5f,
packageName = iconPack,
drawableIds = drawableIds,
) )
} }
} }

View File

@ -5,7 +5,7 @@ import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
class PlaceholderIconProvider(val context: Context) : IconProvider { class PlaceholderIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? { override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
return searchable.getPlaceholderIcon(context) return searchable.getPlaceholderIcon(context)
} }
} }

View File

@ -6,10 +6,9 @@ import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
class SystemIconProvider( class SystemIconProvider(
private val context: Context, private val context: Context
private val legacyIconBackground: Settings.IconSettings.LegacyIconBackground ) : IconProvider {
) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? { override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
return searchable.loadIcon(context, size, legacyIconBackground) return searchable.loadIcon(context, size)
} }
} }

View File

@ -4,13 +4,13 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RotateDrawable
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
@ -37,21 +37,22 @@ internal class ThemedIconProvider(
} }
private suspend fun getGreyscaleIcon(packageName: String): Icon? { private suspend fun getGreyscaleIcon(packageName: String): IconPackIcon? {
val iconDao = AppDatabase.getInstance(context).iconDao() val iconDao = AppDatabase.getInstance(context).iconDao()
return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString()) return iconDao.getGreyscaleIcon(ComponentName(packageName, packageName).flattenToString())
?.let { Icon(it) } ?.let { IconPackIcon(it) }
} }
private fun getStaticIcon(resources: Resources, resId: Int): LauncherIcon? { private fun getStaticIcon(resources: Resources, resId: Int): LauncherIcon? {
try { try {
val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null val fg = ResourcesCompat.getDrawable(resources, resId, null) ?: return null
return LauncherIcon( return StaticLauncherIcon(
foreground = fg, foregroundLayer = TintedIconLayer(
foregroundScale = 0.5f, icon = fg,
background = ColorDrawable(Color.WHITE), scale = 0.5f,
isThemeable = true ),
backgroundLayer = ColorLayer()
) )
} catch (e: Resources.NotFoundException) { } catch (e: Resources.NotFoundException) {
return null return null
@ -64,7 +65,9 @@ internal class ThemedIconProvider(
var i = 0 var i = 0
var drawable: LayerDrawable? = null var drawable: LayerDrawable? = null
var minuteIndex: Int? = null var minuteIndex: Int? = null
var defaultMinute = 0
var hourIndex: Int? = null var hourIndex: Int? = null
var defaultHour = 0
while (i < array.length()) { while (i < array.length()) {
when (array.getString(i)) { when (array.getString(i)) {
"com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> { "com.android.launcher3.LEVEL_PER_TICK_ICON_ROUND" -> {
@ -79,19 +82,46 @@ internal class ThemedIconProvider(
i++ i++
minuteIndex = array.getInt(i, -1).takeIf { it != -1 } 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++ i++
} }
if (drawable != null && minuteIndex != null && hourIndex != null) { if (drawable != null && minuteIndex != null && hourIndex != null) {
return ClockDynamicLauncherIcon(
foreground = drawable, return StaticLauncherIcon(
background = ColorDrawable(Color.WHITE), foregroundLayer = TintedClockLayer(
foregroundScale = 1.5f, sublayers = (0 until drawable.numberOfLayers).map {
backgroundScale = 1f, val drw = drawable.getDrawable(it)
hourLayer = hourIndex, if (drw is RotateDrawable) {
minuteLayer = minuteIndex, drw.level = when (it) {
secondLayer = -1, hourIndex -> {
isThemeable = true, (12 - defaultHour) * 60
}
minuteIndex -> {
(60 - defaultMinute)
}
else -> 0
}
}
ClockSublayer(
drawable = drw,
role = when {
it == hourIndex -> ClockSublayerRole.Hour
it == minuteIndex -> ClockSublayerRole.Minute
else -> ClockSublayerRole.Static
}
)
},
scale = 1.5f,
),
backgroundLayer = ColorLayer()
) )
} }
} catch (e: Resources.NotFoundException) { } catch (e: Resources.NotFoundException) {
@ -108,15 +138,13 @@ internal class ThemedIconProvider(
val array = resources.obtainTypedArrayOrNull(resId) ?: return null val array = resources.obtainTypedArrayOrNull(resId) ?: return null
if (array.length() != 31) return null if (array.length() != 31) return null
return ThemedCalendarDynamicLauncherIcon( return DynamicCalendarIcon(
foregroundScale = 0.5f, resources = resources,
packageName = iconProviderPackage, resourceIds = IntArray(31) {
foregroundIds = IntArray(31) {
array.getResourceId(it, 0).takeIf { it != 0 } ?: return null array.getResourceId(it, 0).takeIf { it != 0 } ?: return null
}, },
background = ColorDrawable(Color.WHITE), isThemed = true
) )
} catch (e: Resources.NotFoundException) { } catch (e: Resources.NotFoundException) {
} }
return null return null

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.icons.providers package de.mm20.launcher2.icons.providers
import android.content.Context import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.Searchable
internal class ThemedPlaceholderIconProvider( internal class ThemedPlaceholderIconProvider(
@ -11,13 +11,30 @@ internal class ThemedPlaceholderIconProvider(
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon { override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
val icon = searchable.getPlaceholderIcon(context) val icon = searchable.getPlaceholderIcon(context)
return LauncherIcon( return StaticLauncherIcon(
foreground = icon.foreground, foregroundLayer = asThemed(icon.foregroundLayer),
foregroundScale = icon.foregroundScale, backgroundLayer = asThemed(icon.backgroundLayer),
background = icon.background,
backgroundScale = icon.backgroundScale,
isThemeable = true
) )
} }
private fun asThemed(layer: LauncherIconLayer): LauncherIconLayer {
return when (layer) {
is ClockLayer -> TintedClockLayer(
scale = layer.scale,
color = 0,
sublayers = layer.sublayers,
)
is ColorLayer -> layer.copy(color = 0)
is StaticIconLayer -> TintedIconLayer(
icon = layer.icon,
color = 0,
scale = layer.scale,
)
is TextLayer -> layer.copy(color = 0)
is TintedIconLayer -> layer.copy(color = 0)
is TintedClockLayer -> return layer.copy(color = 0)
is TransparentLayer -> return layer
}
}
} }

View File

@ -0,0 +1,7 @@
package de.mm20.launcher2.icons.transformations
import de.mm20.launcher2.icons.StaticLauncherIcon
internal interface LauncherIconTransformation {
suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon
}

View File

@ -0,0 +1,50 @@
package de.mm20.launcher2.icons.transformations
import android.graphics.drawable.BitmapDrawable
import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette
import de.mm20.launcher2.icons.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class LegacyToAdaptiveTransformation: LauncherIconTransformation {
override suspend fun transform(icon: StaticLauncherIcon): StaticLauncherIcon {
if (icon.backgroundLayer !is TransparentLayer) return icon
val bgColor = extractColor(icon.foregroundLayer)
return StaticLauncherIcon(
foregroundLayer = scale(icon.foregroundLayer, 0.7f),
backgroundLayer = ColorLayer(bgColor)
)
}
private fun scale(layer: LauncherIconLayer, scale: Float): LauncherIconLayer {
return when(layer) {
is ClockLayer -> layer.copy(scale = scale)
is StaticIconLayer -> layer.copy(scale = scale)
is TintedClockLayer -> layer.copy(scale = scale)
is TintedIconLayer -> layer.copy(scale = scale)
else -> layer
}
}
private suspend fun extractColor(layer: LauncherIconLayer): Int {
if (layer is StaticIconLayer) {
val drawable = layer.icon
val bitmap = if (drawable is BitmapDrawable) {
drawable.bitmap
} else {
drawable.toBitmap(48, 48)
}
val palette = withContext(Dispatchers.Default) {
Palette.from(bitmap).generate()
}
return palette.getDominantColor(0)
} else if (layer is ColorLayer) {
return layer.color
}
return 0
}
}

View File

@ -1,6 +1,8 @@
package de.mm20.launcher2.ktx package de.mm20.launcher2.ktx
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.annotation.Px import androidx.annotation.Px
@ -13,4 +15,11 @@ fun Drawable.toBitmapOrNull(
): Bitmap? { ): Bitmap? {
if (this is BitmapDrawable && bitmap == null) return null if (this is BitmapDrawable && bitmap == null) return null
return toBitmap(width, height, config) return toBitmap(width, height, config)
}
fun Drawable.drawWithColorFilter(canvas: Canvas, colorFilter: ColorFilter?) {
val cf = this.colorFilter
this.colorFilter = colorFilter
this.draw(canvas)
this.colorFilter = cf
} }

View File

@ -0,0 +1,12 @@
package de.mm20.launcher2.ktx
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
fun LayerDrawable.getDrawableOrNull(index: Int): Drawable? {
return try {
this.getDrawable(index)
} catch (e: IndexOutOfBoundsException) {
return null
}
}

View File

@ -1,25 +0,0 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.graphics.drawable.ColorDrawable
import androidx.core.content.ContextCompat
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.R
class MissingPermission(
override val label: String,
val permissionGroup: PermissionGroup,
val secondaryActionLabel: String? = null,
val secondaryAction: (() -> Unit)? = null
): Searchable() {
override val key: String
get() = "permission://${permissionGroup.ordinal}"
override fun getPlaceholderIcon(context: Context): LauncherIcon {
return LauncherIcon(
foreground = ContextCompat.getDrawable(context, R.drawable.ic_permission)!!,
background = ColorDrawable(ContextCompat.getColor(context, R.color.bluegrey))
)
}
}

View File

@ -19,12 +19,11 @@ internal val Context.dataStore: LauncherDataStore by dataStore(
}, },
corruptionHandler = ReplaceFileCorruptionHandler { corruptionHandler = ReplaceFileCorruptionHandler {
CrashReporter.logException(it) CrashReporter.logException(it)
Log.d("MM20", "corruptionHandler")
Settings.getDefaultInstance() Settings.getDefaultInstance()
} }
) )
internal const val SchemaVersion = 6 internal const val SchemaVersion = 7
internal fun getMigrations(context: Context): List<DataMigration<Settings>> { internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
return listOf( return listOf(
@ -34,5 +33,6 @@ internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
Migration_3_4(), Migration_3_4(),
Migration_4_5(), Migration_4_5(),
Migration_5_6(), Migration_5_6(),
Migration_6_7(),
) )
} }

View File

@ -128,7 +128,7 @@ fun createFactorySettings(context: Context): Settings {
) )
.setIcons( .setIcons(
Settings.IconSettings.newBuilder() Settings.IconSettings.newBuilder()
.setLegacyIconBg(Settings.IconSettings.LegacyIconBackground.Dynamic) .setAdaptify(true)
.setShape(Settings.IconSettings.IconShape.PlatformDefault) .setShape(Settings.IconSettings.IconShape.PlatformDefault)
.setThemedIcons(false) .setThemedIcons(false)
.setIconPack("") .setIconPack("")

View File

@ -0,0 +1,12 @@
package de.mm20.launcher2.preferences.migrations
import de.mm20.launcher2.preferences.Settings
class Migration_6_7 : VersionedMigration(6, 7) {
override suspend fun applyMigrations(builder: Settings.Builder): Settings.Builder {
return builder.setIcons(
builder.icons.toBuilder()
.setAdaptify(true)
)
}
}

View File

@ -210,12 +210,7 @@ message Settings {
IconShape shape = 1; IconShape shape = 1;
bool themed_icons = 2; bool themed_icons = 2;
string icon_pack = 3; string icon_pack = 3;
enum LegacyIconBackground { bool adaptify = 5;
Dynamic = 0;
None = 1;
White = 2;
}
LegacyIconBackground legacyIconBg = 4;
} }
IconSettings icons = 21; IconSettings icons = 21;

View File

@ -5,9 +5,9 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.ktx.romanize import de.mm20.launcher2.ktx.romanize
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import de.mm20.launcher2.search.R import de.mm20.launcher2.search.R
import java.text.Collator import java.text.Collator
@ -37,10 +37,9 @@ abstract class Searchable : Comparable<Searchable> {
open suspend fun loadIcon( open suspend fun loadIcon(
context: Context, context: Context,
size: Int, size: Int,
legacyIconBackground: LegacyIconBackground
): LauncherIcon? = null ): LauncherIcon? = null
abstract fun getPlaceholderIcon(context: Context): LauncherIcon abstract fun getPlaceholderIcon(context: Context): StaticLauncherIcon
override fun compareTo(other: Searchable): Int { override fun compareTo(other: Searchable): Int {
return Collator.getInstance().apply { strength = Collator.SECONDARY } return Collator.getInstance().apply { strength = Collator.SECONDARY }

View File

@ -1,14 +1,11 @@
package de.mm20.launcher2.ui.component package de.mm20.launcher2.ui.component
import android.graphics.*
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.Path import android.graphics.Path
import android.graphics.RectF
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas import androidx.compose.foundation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -25,15 +22,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
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 androidx.compose.ui.unit.sp
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.drawWithColorFilter
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
import de.mm20.launcher2.ui.base.LocalTime
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import palettes.TonalPalette
import java.time.Instant
import java.time.ZoneId
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -48,6 +56,25 @@ fun ShapedLauncherIcon(
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
shape: Shape = LocalIconShape.current shape: Shape = LocalIconShape.current
) { ) {
val time = LocalTime.current
var currentIcon by remember(icon) {
mutableStateOf(
when (icon) {
is DynamicLauncherIcon -> null
is StaticLauncherIcon -> icon
null -> null
}
)
}
if (icon is DynamicLauncherIcon) {
LaunchedEffect(time) {
currentIcon = icon.getIcon(time)
}
}
Box( Box(
modifier = modifier modifier = modifier
.size(size) .size(size)
@ -56,7 +83,7 @@ fun ShapedLauncherIcon(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.graphicsLayer { .graphicsLayer {
clip = true clip = currentIcon?.backgroundLayer !is TransparentLayer
this.shape = shape this.shape = shape
} }
.combinedClickable( .combinedClickable(
@ -68,45 +95,19 @@ fun ShapedLauncherIcon(
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (icon != null) { currentIcon?.let {
IconLayer(
val fgScale = icon.foregroundScale it.backgroundLayer,
val bgScale = icon.backgroundScale size,
colorTone = if (!LocalDarkTheme.current) 30 else 90,
val themedFgColor = MaterialTheme.colorScheme.onPrimaryContainer MaterialTheme.colorScheme.primaryContainer
val themedBgColor = MaterialTheme.colorScheme.primaryContainer )
IconLayer(
val fg = remember(icon, icon.isThemeable, themedFgColor) { it.foregroundLayer,
icon.foreground.also { size,
if (icon.isThemeable) it.setTint(themedFgColor.toArgb()) colorTone = if (!LocalDarkTheme.current) 90 else 10,
} MaterialTheme.colorScheme.onPrimaryContainer
} )
val bg = remember(icon, icon.isThemeable, themedBgColor) {
icon.background?.also {
if (icon.isThemeable) it.setTint(themedBgColor.toArgb())
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
drawIntoCanvas {
val paddingFg = (size * (1 - fgScale) * 0.5f).toPx()
val paddingBg = (size * (1 - bgScale) * 0.5f).toPx()
bg?.setBounds(
paddingBg.toInt(),
paddingBg.toInt(),
(this.size.width - paddingBg).toInt(),
(this.size.height - paddingBg).toInt()
)
bg?.draw(it.nativeCanvas)
fg.setBounds(
paddingFg.toInt(),
paddingFg.toInt(),
(this.size.width - paddingFg).toInt(),
(this.size.height - paddingFg).toInt()
)
fg.draw(it.nativeCanvas)
}
}
} }
} }
if (badge != null) { if (badge != null) {
@ -138,12 +139,18 @@ fun ShapedLauncherIcon(
val number = badge.number val number = badge.number
if (badgeIconRes != null) { if (badgeIconRes != null) {
Image( Image(
modifier = Modifier.fillMaxSize().padding(size / 48), modifier = Modifier
.fillMaxSize()
.padding(size / 48),
painter = painterResource(badgeIconRes), painter = painterResource(badgeIconRes),
contentDescription = null contentDescription = null
) )
} else if (badgeIcon != null) { } else if (badgeIcon != null) {
Canvas(modifier = Modifier.fillMaxSize().padding(size / 48)) { Canvas(
modifier = Modifier
.fillMaxSize()
.padding(size / 48)
) {
badgeIcon.setBounds( badgeIcon.setBounds(
0, 0,
0, 0,
@ -171,6 +178,172 @@ fun ShapedLauncherIcon(
} }
} }
@Composable
private fun IconLayer(
layer: LauncherIconLayer,
size: Dp,
colorTone: Int,
defaultTintColor: Color
) {
when (layer) {
is ClockLayer -> {
ClockLayer(layer.sublayers, scale = layer.scale, tintColor = null)
}
is TintedClockLayer -> {
ClockLayer(
layer.sublayers,
scale = layer.scale,
tintColor = if (layer.color == 0) defaultTintColor
else Color(getTone(layer.color, colorTone))
)
}
is ColorLayer -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (layer.color == 0) {
defaultTintColor
} else {
Color(getTone(layer.color, colorTone))
}
)
)
}
is StaticIconLayer -> {
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
this.scale(layer.scale)
}) {
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.draw(it.nativeCanvas)
}
}
}
}
is TextLayer -> {
Text(
text = layer.text,
style = MaterialTheme.typography.headlineSmall.copy(
fontSize = 20.sp * (size / 48.dp)
),
color = if (layer.color == 0) {
defaultTintColor
} else {
Color(getTone(layer.color, colorTone))
},
)
}
is TintedIconLayer -> {
val color =
if (layer.color == 0) defaultTintColor.toArgb()
else getTone(layer.color, colorTone)
Canvas(modifier = Modifier.fillMaxSize()) {
withTransform({
this.scale(layer.scale)
}) {
drawIntoCanvas {
layer.icon.bounds = this.size.toRect().toAndroidRect()
layer.icon.drawWithColorFilter(
it.nativeCanvas,
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
)
}
}
}
}
is TransparentLayer -> {}
}
}
private fun getTone(argb: Int, tone: Int): Int {
return TonalPalette
.fromInt(argb)
.tone(tone)
}
@Composable
private fun ClockLayer(
sublayers: List<ClockSublayer>,
scale: Float,
tintColor: Color?,
) {
val time = remember {
Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneId.systemDefault())
}
val transition = rememberInfiniteTransition()
val minute by transition.animateFloat(
initialValue = 0f,
targetValue = 60f,
animationSpec = InfiniteRepeatableSpec(
animation = tween(durationMillis = 60 * 60 * 1000, easing = LinearEasing),
initialStartOffset = StartOffset(
offsetMillis = time.minute * 60 * 1000 + time.second * 1000,
offsetType = StartOffsetType.FastForward
)
)
)
val hour by transition.animateFloat(
initialValue = 0f,
targetValue = 12f,
animationSpec = InfiniteRepeatableSpec(
animation = tween(durationMillis = 12 * 60 * 60 * 1000, easing = LinearEasing),
initialStartOffset = StartOffset(
offsetMillis = (time.hour % 12) * 60 * 60 * 1000 + time.minute * 60 * 1000 + time.second * 1000,
offsetType = StartOffsetType.FastForward
)
)
)
val second by transition.animateFloat(
initialValue = 0f,
targetValue = 60f,
animationSpec = InfiniteRepeatableSpec(
animation = tween(durationMillis = 60000, easing = LinearEasing),
initialStartOffset = StartOffset(
offsetMillis = time.second * 1000,
offsetType = StartOffsetType.FastForward
)
)
)
Canvas(modifier = Modifier.fillMaxSize()) {
val colorFilter = tintColor?.let {
PorterDuffColorFilter(tintColor.toArgb(), PorterDuff.Mode.SRC_IN)
}
withTransform({
this.scale(scale)
}) {
for (sublayer in sublayers) {
withTransform({
when (sublayer.role) {
ClockSublayerRole.Hour -> {
rotate(hour / 12f * 360f)
}
ClockSublayerRole.Minute -> {
rotate(minute / 60f * 360f)
}
ClockSublayerRole.Second -> {
rotate(second / 60f * 360f)
}
ClockSublayerRole.Static -> {}
}
}) {
drawIntoCanvas {
sublayer.drawable.bounds = this.size.toRect().toAndroidRect()
sublayer.drawable.drawWithColorFilter(it.nativeCanvas, colorFilter)
}
}
}
}
}
}
val LocalIconShape = compositionLocalOf<Shape> { CircleShape } val LocalIconShape = compositionLocalOf<Shape> { CircleShape }
fun getShape(iconShape: IconShape): Shape { fun getShape(iconShape: IconShape): Shape {

View File

@ -1,145 +0,0 @@
package de.mm20.launcher2.ui.icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.colorResource
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.*
data class PlaceholderIcon(
val color: Color,
val icon: ImageVector
)
@Composable
fun Searchable.getPlaceholderIcon(): PlaceholderIcon {
return when (this) {
is Application -> getPlaceholderIcon()
is File -> getPlaceholderIcon()
else -> PlaceholderIcon(
Color.LightGray,
Icons.Rounded.Circle
)
}
}
@Composable
fun Application.getPlaceholderIcon(): PlaceholderIcon {
return PlaceholderIcon(
colorResource(id = R.color.android_green),
Icons.Rounded.Android
)
}
@Composable
fun File.getPlaceholderIcon(): PlaceholderIcon {
return when {
isDirectory -> PlaceholderIcon(
colorResource(id = R.color.lightblue),
Icons.Rounded.Folder
)
mimeType.startsWith("image/") -> PlaceholderIcon(
colorResource(id = R.color.teal),
Icons.Rounded.Image
)
mimeType.startsWith("audio/") -> PlaceholderIcon(
colorResource(id = R.color.orange),
Icons.Rounded.Audiotrack
)
mimeType.startsWith("video/") -> PlaceholderIcon(
colorResource(id = R.color.purple),
Icons.Rounded.Movie
)
/*
else -> when (mimeType) {
"application/vnd.google-apps.drawing" -> R.drawable.ic_file_picture to R.color.teal
}*/
else -> when (mimeType) {
"application/pdf" -> PlaceholderIcon(
colorResource(id = R.color.red),
Icons.Rounded.Pdf
)
"application/zip",
"application/x-gtar",
"application/x-tar",
"application/java-archive",
"application/x-7z-compressed",
"application/x-compressed-tar",
"application/x-zip-compressed",
"application/x-gzip",
"application/x-bzip2" -> PlaceholderIcon(
colorResource(id = R.color.brown),
Icons.Rounded.Archive
)
"application/vnd.oasis.opendocument.text",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
"text/plain",
"application/x-iwork-pages-sffpages",
"application/vnd.apple.pages",
"application/vnd.google-apps.document" -> PlaceholderIcon(
colorResource(id = R.color.blue),
Icons.Rounded.Notes
)
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-excel",
"application/x-iwork-numbers-sffnumbers",
"application/vnd.apple.numbers",
"application/vnd.google-apps.spreadsheet" -> PlaceholderIcon(
colorResource(id = R.color.lightgreen),
Icons.Rounded.BorderAll
)
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.ms-powerpoint",
"application/x-iwork-keynote-sffkey",
"application/vnd.apple.keynote",
"application/vnd.google-apps.presentation" -> PlaceholderIcon(
colorResource(id = R.color.amber),
Icons.Rounded.Slideshow
)
"application/vnd.android.package-archive" -> PlaceholderIcon(
colorResource(id = R.color.android_green),
Icons.Rounded.Android
)
"text/x-asm",
"text/x-c",
"text/x-java-source",
"text/x-script.phyton",
"text/x-pascal",
"text/x-script.perl",
"text/javascript",
"application/json" -> PlaceholderIcon(
colorResource(id = R.color.pink),
Icons.Rounded.Code
)
"text/xml",
"text/html" -> PlaceholderIcon(
colorResource(id = R.color.deeporange),
Icons.Rounded.Code
)
"application/vnd.google-apps.form" -> PlaceholderIcon(
colorResource(id = R.color.deeppurple),
Icons.Rounded.ViewList
)
"application/epub+zip" -> PlaceholderIcon(
colorResource(id = R.color.blue),
Icons.Rounded.Book
)
else -> PlaceholderIcon(
colorResource(id = R.color.bluegrey),
Icons.Rounded.InsertDriveFile
)
}
}
}

View File

@ -31,7 +31,6 @@ import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.customView
import com.android.launcher3.GestureNavContract import com.android.launcher3.GestureNavContract
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.icons.DynamicIconController
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.base.BaseActivity import de.mm20.launcher2.ui.base.BaseActivity
@ -45,7 +44,6 @@ import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
import de.mm20.launcher2.ui.locals.LocalWindowSize import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.theme.LauncherTheme import de.mm20.launcher2.ui.theme.LauncherTheme
import org.koin.android.ext.android.inject
class LauncherActivity : BaseActivity() { class LauncherActivity : BaseActivity() {
@ -174,10 +172,6 @@ class LauncherActivity : BaseActivity() {
editFavoritesDialog = null editFavoritesDialog = null
} }
} }
val dynamicIconController: DynamicIconController by inject()
lifecycle.addObserver(dynamicIconController)
} }
override fun onAttachedToWindow() { override fun onAttachedToWindow() {

View File

@ -1,31 +1,10 @@
package de.mm20.launcher2.ui.legacy.view package de.mm20.launcher2.ui.legacy.view
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewConfiguration
import androidx.core.content.ContextCompat
import com.bartoszlipinski.viewpropertyobjectanimator.ViewPropertyObjectAnimator
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.toRectF
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.IconSettings.IconShape
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.legacy.helper.BitmapHolder
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.lang.Math.pow
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.roundToInt
class LauncherIconView : View, KoinComponent { class LauncherIconView : View, KoinComponent {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
@ -36,499 +15,10 @@ class LauncherIconView : View, KoinComponent {
defStyleRes defStyleRes
) )
var shape: IconShape
set(value) {
if (value == IconShape.PlatformDefault) {
platformShape = getSystemShape()
transformMatrix = Matrix()
platformShapeBounds = RectF()
field = value
} else {
platformShape = null
transformMatrix = null
platformShapeBounds = null
field = value
}
}
private var platformShape: Path? = null
private var transformMatrix: Matrix? = null
private var platformShapeBounds: RectF? = null
private fun getSystemShape(): Path {
return AdaptiveIconDrawable(null, null).iconMask
}
var icon: LauncherIcon? = null var icon: LauncherIcon? = null
set(value) { set(value) {
field = value field = value
foregroundScale = value?.foregroundScale ?: 1f
backgroundScale = value?.backgroundScale ?: 1f
value?.registerCallback(iconObserver)
invalidate() invalidate()
} }
private val iconObserver: (LauncherIcon) -> Unit = {
foregroundScale = it.foregroundScale
backgroundScale = it.backgroundScale
// Implicit invalidate
}
var foregroundScale = 1f
set(value) {
field = value
postInvalidate()
}
var backgroundScale = 1f
set(value) {
field = value
postInvalidate()
}
init {
shape = IconShape.Circle
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun setElevation(elevation: Float) {
super.setElevation(elevation)
shadowPaint = updateShadowPaint()
badgeShadowPaint = updateBadgeShadowPaing()
}
override fun setTranslationZ(translationZ: Float) {
super.setTranslationZ(translationZ)
shadowPaint = updateShadowPaint()
badgeShadowPaint = updateBadgeShadowPaing()
}
private var shadowPaint: Paint = updateShadowPaint()
private var badgeShadowPaint = updateBadgeShadowPaing()
private fun updateShadowPaint(): Paint {
return Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER)
color = Color.TRANSPARENT
isAntiAlias = true
setShadowLayer(0.5f * z, 0f, 0.5f * z, 0x40000000)
}
}
private fun updateBadgeShadowPaing(): Paint {
return Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)
color = Color.TRANSPARENT
isAntiAlias = true
setShadowLayer(0.5f * z, 0f, 0.5f * z, 0x40000000)
}
}
private val drawRect = Rect()
private val bmpDrawRect = Rect()
private val maskPaint = Paint().apply {
style = Paint.Style.FILL
color = 0xFF000000.toInt()
isAntiAlias = true
}
private val bitmapPaint = Paint().apply {
color = 0xFF000000.toInt()
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
isAntiAlias = true
isFilterBitmap = true
}
private val badgeTextPaint = Paint().apply {
color = ContextCompat.getColor(context, R.color.badge_text)
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private val badgeProgressPaint = Paint().apply {
color = 0x30000000
isAntiAlias = true
textAlign = Paint.Align.CENTER
}
private var path: Path = Path()
private val badgeRect = RectF()
private val textBounds = Rect()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val fg = icon?.foreground ?: return
val bg = icon?.background
canvas.getClipBounds(drawRect)
drawRect.left += paddingLeft
drawRect.top += paddingTop
drawRect.right -= paddingRight
drawRect.bottom -= paddingBottom
val (bmp, c) = BitmapHolder.getBitmapAndCanvas((drawRect.width() * 1.8).toInt())
c.getClipBounds(bmpDrawRect)
fg.bounds = bmpDrawRect
if (bg != null) {
bg.bounds = bmpDrawRect
when (shape) {
IconShape.PlatformDefault -> {
path.rewind()
val matrix = transformMatrix!!
val bounds = platformShapeBounds!!
val shape = platformShape!!
shape.computeBounds(bounds, true)
matrix.setRectToRect(
bounds,
badgeRect.also { drawRect.toRectF(it) },
Matrix.ScaleToFit.CENTER
)
path.rewind()
shape.transform(matrix, path)
canvas.drawPath(path, maskPaint)
}
IconShape.Circle -> {
canvas.drawOval(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
maskPaint
)
}
IconShape.Square -> {
canvas.drawRect(drawRect, maskPaint)
}
IconShape.RoundedSquare -> {
canvas.drawRoundRect(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
maskPaint
)
}
IconShape.Triangle -> {
path.rewind()
var cx = drawRect.left.toFloat()
var cy = drawRect.top + drawRect.height().toFloat() * 0.86f
val r = drawRect.width()
path.moveTo(cx, cy)
path.arcTo(cx - r, cy - r, cx + r, cy + r, 300f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 300f, 60f, true, maskPaint)
cx = drawRect.right.toFloat()
cy = drawRect.top + drawRect.height().toFloat() * 0.86f
path.lineTo(cx, cy)
path.arcTo(cx - r, cy - r, cx + r, cy + r, 180f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 180f, 60f, true, maskPaint)
cx = drawRect.left + drawRect.width() * 0.5f
cy = drawRect.top.toFloat()
path.lineTo(cx, cy)
path.close()
path.arcTo(cx - r, cy - r, cx + r, cy + r, 60f, 60f, true)
canvas.drawArc(cx - r, cy - r, cx + r, cy + r, 60f, 60f, true, maskPaint)
}
IconShape.Squircle -> {
path.rewind()
val radius = drawRect.width() / 2
val radiusToPow = pow(radius.toDouble(), 3.0)
path.moveTo(-radius.toFloat(), 0f)
for (x in -radius..radius)
path.lineTo(
x.toFloat(),
Math.cbrt(radiusToPow - Math.abs(x * x * x)).toFloat()
)
for (x in radius downTo -radius)
path.lineTo(
x.toFloat(),
(-Math.cbrt(radiusToPow - Math.abs(x * x * x))).toFloat()
)
path.close()
canvas.save()
canvas.translate(width / 2f, height / 2f)
canvas.drawPath(path, maskPaint)
canvas.restore()
}
IconShape.Hexagon -> {
path.rewind()
path.moveTo(
drawRect.left + drawRect.width() * 0.25f,
drawRect.top + drawRect.height() * 0.933f
)
path.lineTo(
drawRect.left + drawRect.width() * 0.75f,
drawRect.top + drawRect.height() * 0.933f
)
path.lineTo(
drawRect.left + drawRect.width() * 1.0f,
drawRect.top + drawRect.height() * 0.5f
)
path.lineTo(
drawRect.left + drawRect.width() * 0.75f,
drawRect.top + drawRect.height() * 0.067f
)
path.lineTo(
drawRect.left + drawRect.width() * 0.25f,
drawRect.top + drawRect.height() * 0.067f
)
path.lineTo(drawRect.left.toFloat(), drawRect.top + drawRect.height() * 0.5f)
path.close()
canvas.drawPath(path, maskPaint)
}
IconShape.EasterEgg -> {
path.rewind()
path.moveTo(
0.49999999f * drawRect.width() + drawRect.left,
1f * drawRect.height() + drawRect.top
)
path.lineTo(
0.42749999f * drawRect.width() + drawRect.left,
0.9339999999999999f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.16999998f * drawRect.width() + drawRect.left,
0.7005004f * drawRect.height() + drawRect.top,
0f + drawRect.left,
0.5460004f * drawRect.height() + drawRect.top,
0f + drawRect.left,
0.3575003f * drawRect.height() + drawRect.top
)
path.cubicTo(
0f + drawRect.left,
0.2030004f * drawRect.height() + drawRect.top,
0.12100002f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top,
0.275f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.362f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top,
0.4455f * drawRect.width() + drawRect.left,
0.123f * drawRect.height() + drawRect.top,
0.5f * drawRect.width() + drawRect.left,
0.1865003f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.55449999f * drawRect.width() + drawRect.left,
0.123f * drawRect.height() + drawRect.top,
0.638f * drawRect.width() + drawRect.left,
0.0825f * drawRect.height() + drawRect.top,
0.725f * drawRect.width() + drawRect.left,
0.0825f * drawRect.height() + drawRect.top
)
path.cubicTo(
0.87900006f * drawRect.width() + drawRect.left,
0.0825004f * drawRect.height() + drawRect.top,
1f * drawRect.width() + drawRect.left,
0.2030004f * drawRect.height() + drawRect.top,
1f * drawRect.width() + drawRect.left,
0.3575003f * drawRect.height() + drawRect.top
)
path.cubicTo(
1f * drawRect.width() + drawRect.left,
0.5460004f * drawRect.height() + drawRect.top,
0.82999999f * drawRect.width() + drawRect.left,
0.7005004f * drawRect.height() + drawRect.top,
0.57250001f * drawRect.width() + drawRect.left,
0.9340004f * drawRect.height() + drawRect.top
)
path.close()
canvas.drawPath(path, maskPaint)
}
IconShape.Pentagon -> {
path.rewind()
path.moveTo(
0.49997027f * drawRect.width() + drawRect.left,
0.0060308f * drawRect.height() + drawRect.top
)
path.lineTo(
0.99994053f * drawRect.width() + drawRect.left,
0.36928048f * drawRect.height() + drawRect.top
)
path.lineTo(
0.80896887f * drawRect.width() + drawRect.left,
0.95703078f * drawRect.height() + drawRect.top
)
path.lineTo(
0.19097162f * drawRect.width() + drawRect.left,
0.95703076f * drawRect.height() + drawRect.top
)
path.lineTo(
drawRect.left.toFloat(),
0.36928045f * drawRect.height() + drawRect.top
)
path.close()
canvas.drawPath(path, maskPaint)
}
}
c.save()
c.scale(
backgroundScale,
backgroundScale,
bmpDrawRect.centerX().toFloat(),
bmpDrawRect.centerY().toFloat()
)
bg.draw(c)
c.restore()
}
c.save()
c.scale(
foregroundScale,
foregroundScale,
bmpDrawRect.centerX().toFloat(),
bmpDrawRect.centerY().toFloat()
)
fg.draw(c)
c.restore()
if (bg != null) {
canvas.drawBitmap(bmp, bmpDrawRect, drawRect, bitmapPaint)
} else {
canvas.drawBitmap(bmp, bmpDrawRect, drawRect, maskPaint)
}
if (bg != null) {
when (shape) {
IconShape.Circle -> {
canvas.drawOval(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
shadowPaint
)
}
IconShape.Square -> {
canvas.drawRect(drawRect, shadowPaint)
}
IconShape.RoundedSquare -> {
canvas.drawRoundRect(
drawRect.left.toFloat(),
drawRect.top.toFloat(),
drawRect.right.toFloat(),
drawRect.bottom.toFloat(),
width * 0.125f,
height * 0.125f,
shadowPaint
)
}
IconShape.Triangle, IconShape.Hexagon, IconShape.EasterEgg, IconShape.Pentagon, IconShape.PlatformDefault -> {
canvas.drawPath(path, shadowPaint)
}
IconShape.Squircle -> {
canvas.save()
canvas.translate(width / 2f, height / 2f)
canvas.drawPath(path, shadowPaint)
canvas.restore()
}
}
}
}
private var longClicked = false
private val longClickRunnable = Runnable {
longClicked = true
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
performLongClick()
}
private var downX = 0f
private var downY = 0f
override fun onTouchEvent(ev: MotionEvent): Boolean {
if (!hasOnClickListeners()) return false
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
animateTouchDown()
downX = ev.rawX
downY = ev.rawY
longClicked = false
handler?.postDelayed(
longClickRunnable,
ViewConfiguration.getLongPressTimeout().toLong()
)
return true
}
MotionEvent.ACTION_MOVE -> {
if (abs(hypot(downX - ev.rawX, downY - ev.rawY)) > width * 0.25f) {
handler?.removeCallbacks(longClickRunnable)
animateTouchUp()
return false
}
}
MotionEvent.ACTION_UP -> {
animateTouchUp()
if (ev.x > 0 && ev.x < width && ev.y > 0 && ev.y < height && !longClicked) {
performClick()
}
handler?.removeCallbacks(longClickRunnable)
return false
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_OUTSIDE -> {
animateTouchUp()
handler?.removeCallbacks(longClickRunnable)
return false
}
}
return true
}
private fun animateTouchUp() {
AnimatorSet().also {
it.playTogether(
ViewPropertyObjectAnimator.animate(this).translationZ(0f).get(),
ObjectAnimator.ofFloat(this, "foregroundScale", icon?.foregroundScale ?: 1f),
ObjectAnimator.ofFloat(this, "backgroundScale", icon?.backgroundScale ?: 1f)
)
it.duration = 300
it.start()
}
}
private fun animateTouchDown() {
AnimatorSet().also {
it.playTogether(
ViewPropertyObjectAnimator.animate(this).translationZ(2 * dp).get(),
ObjectAnimator.ofFloat(
this, "foregroundScale", (icon?.foregroundScale
?: 1f) * 0.8f
),
ObjectAnimator.ofFloat(
this, "backgroundScale", (icon?.backgroundScale
?: 1f) * 1.2f
)
)
it.duration = 250
it.start()
}
}
companion object: KoinComponent {
var currentShape: IconShape = IconShape.PlatformDefault
fun getDefaultShape(): Flow<IconShape> = channelFlow {
send(currentShape)
val dataStore: LauncherDataStore = get()
dataStore.data.map { it.icons.shape }.distinctUntilChanged().collectLatest { shape ->
dataStore.data.map { it.easterEgg }.distinctUntilChanged().collectLatest { ee ->
if (ee) {
currentShape = IconShape.EasterEgg
send(IconShape.EasterEgg)
} else {
currentShape = shape
send(shape)
}
}
}
}
}
} }

View File

@ -1,17 +1,12 @@
package de.mm20.launcher2.ui.settings.appearance package de.mm20.launcher2.ui.settings.appearance
import android.graphics.drawable.ColorDrawable
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@ -24,6 +19,7 @@ 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.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.airbnb.lottie.LottieProperty import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.compose.* import com.airbnb.lottie.compose.*
@ -31,7 +27,9 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.HorizontalPagerIndicator import com.google.accompanist.pager.HorizontalPagerIndicator
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.preferences.Settings.* import de.mm20.launcher2.preferences.Settings.*
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
@ -58,8 +56,8 @@ fun AppearanceSettingsScreen() {
title = stringResource(id = R.string.preference_layout), title = stringResource(id = R.string.preference_layout),
summary = stringResource(id = R.string.preference_layout_summary), summary = stringResource(id = R.string.preference_layout_summary),
value = layout, onValueChanged = { value = layout, onValueChanged = {
viewModel.setLayout(it) viewModel.setLayout(it)
}) })
val theme by viewModel.theme.observeAsState() val theme by viewModel.theme.observeAsState()
ListPreference( ListPreference(
title = stringResource(id = R.string.preference_theme), title = stringResource(id = R.string.preference_theme),
@ -147,6 +145,15 @@ fun AppearanceSettingsScreen() {
viewModel.setIconShape(it) viewModel.setIconShape(it)
} }
) )
val adaptifyLegacyIcons by viewModel.adaptifyLegacyIcons.observeAsState()
SwitchPreference(
title = stringResource(R.string.preference_enforce_icon_shape),
summary = stringResource(R.string.preference_enforce_icon_shape_summary),
value = adaptifyLegacyIcons == true,
onValueChanged = {
viewModel.setAdaptifyLegacyIcons(it)
}
)
val themedIcons by viewModel.themedIcons.observeAsState() val themedIcons by viewModel.themedIcons.observeAsState()
SwitchPreference( SwitchPreference(
title = stringResource(R.string.preference_themed_icons), title = stringResource(R.string.preference_themed_icons),
@ -176,17 +183,6 @@ fun AppearanceSettingsScreen() {
if (it != null) viewModel.setIconPack(it) if (it != null) viewModel.setIconPack(it)
} }
) )
val legacyIconBackground by viewModel.legacyIconBackground.observeAsState()
LegacyIconBackgroundPreference(
title = stringResource(R.string.preference_legacy_icon_bg),
summary = stringResource(R.string.preference_legacy_icon_bg_summary),
value = legacyIconBackground,
onValueChanged = {
viewModel.setLegacyIconBackground(it)
},
iconShape = iconShape
)
} }
PreferenceCategory(stringResource(R.string.preference_category_searchbar)) { PreferenceCategory(stringResource(R.string.preference_category_searchbar)) {
val searchBarStyle by viewModel.searchBarStyle.observeAsState() val searchBarStyle by viewModel.searchBarStyle.observeAsState()
@ -370,13 +366,15 @@ fun IconShapePreference(
) { ) {
ShapedLauncherIcon( ShapedLauncherIcon(
size = 48.dp, size = 48.dp,
icon = LauncherIcon( icon = StaticLauncherIcon(
foreground = AppCompatResources.getDrawable( foregroundLayer = StaticIconLayer(
LocalContext.current, icon = ContextCompat.getDrawable(
R.mipmap.ic_launcher_foreground LocalContext.current,
)!!, R.mipmap.ic_launcher_foreground
foregroundScale = 1.5f, )!!,
background = ColorDrawable(LocalContext.current.getColor(R.color.ic_launcher_background)) scale = 1.5f,
),
ColorLayer(LocalContext.current.getColor(R.color.ic_launcher_background))
), ),
onClick = { onClick = {
onValueChanged(it) onValueChanged(it)
@ -399,82 +397,6 @@ fun IconShapePreference(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LegacyIconBackgroundPreference(
title: String,
summary: String? = null,
value: IconSettings.LegacyIconBackground?,
onValueChanged: (IconSettings.LegacyIconBackground) -> Unit,
iconShape: IconSettings.IconShape
) {
var showDialog by remember { mutableStateOf(false) }
Preference(title = title, summary = summary, onClick = { showDialog = true })
if (showDialog && value != null) {
val colors = remember {
IconSettings.LegacyIconBackground.values()
.filter { it != IconSettings.LegacyIconBackground.UNRECOGNIZED }
}
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
tonalElevation = 16.dp,
shadowElevation = 16.dp,
shape = MaterialTheme.shapes.extraLarge,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(
start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp
)
)
LazyVerticalGrid(
columns = GridCells.Adaptive(96.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp)
) {
items(colors) {
Column(
modifier = Modifier
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
ShapedLauncherIcon(
size = 48.dp,
icon = LauncherIcon(
foreground = AppCompatResources.getDrawable(
LocalContext.current,
R.mipmap.ic_launcher_foreground
)!!,
background = null,
autoGenerateBackgroundMode = when (it) {
IconSettings.LegacyIconBackground.Dynamic -> LauncherIcon.BACKGROUND_DYNAMIC
IconSettings.LegacyIconBackground.None -> LauncherIcon.BACKGROUND_NONE
else -> LauncherIcon.BACKGROUND_WHITE
}
),
onClick = {
onValueChanged(it)
showDialog = false
}
)
}
}
}
}
}
}
}
}
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
fun LayoutPreference( fun LayoutPreference(

View File

@ -9,7 +9,6 @@ import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme import de.mm20.launcher2.preferences.Settings.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import de.mm20.launcher2.preferences.Settings.SearchBarSettings import de.mm20.launcher2.preferences.Settings.SearchBarSettings
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -108,14 +107,14 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
} }
} }
val legacyIconBackground = dataStore.data.map { it.icons.legacyIconBg }.asLiveData() val adaptifyLegacyIcons = dataStore.data.map { it.icons.adaptify }.asLiveData()
fun setLegacyIconBackground(legacyIconBackground: LegacyIconBackground) { fun setAdaptifyLegacyIcons(adaptify: Boolean) {
viewModelScope.launch { viewModelScope.launch {
dataStore.updateData { dataStore.updateData {
it.toBuilder() it.toBuilder()
.setIcons( .setIcons(
it.icons.toBuilder() it.icons.toBuilder()
.setLegacyIconBg(legacyIconBackground) .setAdaptify(adaptify)
) )
.build() .build()
} }

View File

@ -3,22 +3,13 @@ package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import androidx.core.graphics.drawable.toBitmap import androidx.core.content.ContextCompat
import androidx.palette.graphics.Palette
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import de.mm20.launcher2.graphics.TextDrawable import de.mm20.launcher2.icons.*
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.ktx.sp
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import de.mm20.launcher2.websites.R import de.mm20.launcher2.websites.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
class Website( class Website(
@ -31,7 +22,10 @@ class Website(
) : Searchable() { ) : Searchable() {
override val key = "web://$url" override val key = "web://$url"
override suspend fun loadIcon(context: Context, size: Int, legacyIconBackground: LegacyIconBackground): LauncherIcon? { override suspend fun loadIcon(
context: Context,
size: Int,
): LauncherIcon? {
if (favicon.isEmpty()) return null if (favicon.isEmpty()) return null
try { try {
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
@ -40,11 +34,13 @@ class Website(
.allowHardware(false) .allowHardware(false)
.build() .build()
val icon = context.imageLoader.execute(request).drawable ?: return null val icon = context.imageLoader.execute(request).drawable ?: return null
return LauncherIcon(
foreground = icon, return StaticLauncherIcon(
background = color.let { ColorDrawable(it) }, foregroundLayer = StaticIconLayer(
foregroundScale = 0.7f, icon = icon,
autoGenerateBackgroundMode = legacyIconBackground.number scale = 0.7f,
),
backgroundLayer = ColorLayer(color)
) )
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
return null return null
@ -52,19 +48,20 @@ class Website(
} }
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val drawable = if (label.isNotEmpty()) { if (label.isNotBlank()) {
TextDrawable( return StaticLauncherIcon(
label[0].toString(), foregroundLayer = TextLayer(text = label[0].toString(), color = Color.WHITE),
typeface = Typeface.DEFAULT_BOLD, backgroundLayer = ColorLayer(Color.LTGRAY)
fontSize = 40 * context.sp,
height = (48 * context.dp).toInt()
) )
} else context.getDrawable(R.drawable.ic_website)!! }
return LauncherIcon(
foreground = drawable, return StaticLauncherIcon(
background = ColorDrawable(if (color != 0) color else Color.LTGRAY), foregroundLayer = StaticIconLayer(
foregroundScale = 1f icon = ContextCompat.getDrawable(context, R.drawable.ic_website)!!,
scale = 0.5f,
),
backgroundLayer = ColorLayer(if (color != 0) color else Color.LTGRAY)
) )
} }

View File

@ -3,46 +3,40 @@ package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.text.Spanned
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.text.HtmlCompat import androidx.core.content.ContextCompat
import androidx.core.text.toHtml import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticIconLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.wikipedia.R import de.mm20.launcher2.wikipedia.R
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.helper.NetworkUtils
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import kotlin.math.min
class Wikipedia( class Wikipedia(
override val label: String, override val label: String,
val id: Long, val id: Long,
val text: String, val text: String,
val image: String?, val image: String?,
val wikipediaUrl: String, val wikipediaUrl: String,
) : Searchable() { ) : Searchable() {
override val key = "wikipedia://$wikipediaUrl:$id" override val key = "wikipedia://$wikipediaUrl:$id"
override fun getPlaceholderIcon(context: Context): LauncherIcon { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return LauncherIcon( return StaticLauncherIcon(
foreground = context.getDrawable(R.drawable.ic_wikipedia)!!, foregroundLayer = StaticIconLayer(
background = ColorDrawable(0xFFF0F0F0.toInt()) icon = ContextCompat.getDrawable(context, R.drawable.ic_wikipedia)!!,
scale = 1f
),
backgroundLayer = ColorLayer(0xFFF0F0F0.toInt())
) )
} }
override fun getLaunchIntent(context: Context): Intent? { override fun getLaunchIntent(context: Context): Intent? {
val intent = CustomTabsIntent val intent = CustomTabsIntent
.Builder() .Builder()
.setToolbarColor(Color.BLACK) .setToolbarColor(Color.BLACK)
.enableUrlBarHiding() .enableUrlBarHiding()
.setShowTitle(true) .setShowTitle(true)
.build() .build()
val uri = "${wikipediaUrl}/wiki?curid=$id" val uri = "${wikipediaUrl}/wiki?curid=$id"
intent.intent.data = Uri.parse(uri) intent.intent.data = Uri.parse(uri)
return intent.intent return intent.intent