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.Intent
import android.content.pm.PackageInstaller
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import androidx.core.content.ContextCompat
import de.mm20.launcher2.applications.R
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import de.mm20.launcher2.icons.*
class AppInstallation(
val session: PackageInstaller.SessionInfo
val session: PackageInstaller.SessionInfo
) : Application(
label = session.appLabel?.toString() ?: "",
`package` = session.appPackageName ?: "",
activity = "",
flags = 0,
version = null
label = session.appLabel?.toString() ?: "",
`package` = session.appPackageName ?: "",
activity = "",
flags = 0,
version = null
) {
override val key: String
@ -30,22 +28,31 @@ class AppInstallation(
return session.createDetailsIntent()
}
override fun getPlaceholderIcon(context: Context): LauncherIcon {
return LauncherIcon(
foreground = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
background = ColorDrawable(ContextCompat.getColor(context, R.color.grey)),
foregroundScale = 0.5f)
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
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 foreground = BitmapDrawable(context.resources, icon)
foreground.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
setSaturation(0f)
})
return LauncherIcon(
foreground = foreground,
background = ColorDrawable(ContextCompat.getColor(context, R.color.grey))
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
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.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable
import android.util.Log
import android.graphics.Color
import androidx.core.content.ContextCompat
import de.mm20.launcher2.applications.R
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
abstract class Application(
@ -34,11 +35,14 @@ abstract class Application(
return intent
}
override fun getPlaceholderIcon(context: Context): LauncherIcon {
return LauncherIcon(
foreground = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
background = ColorDrawable(ContextCompat.getColor(context, R.color.lightgreen)),
foregroundScale = 0.5f
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
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"
companion object {
internal fun getStoreLinkForInstaller(installerPackage: String?, packageName: String?): StoreLink? {
internal fun getStoreLinkForInstaller(
installerPackage: String?,
packageName: String?
): StoreLink? {
if (packageName == null) return null
return when (installerPackage) {
"de.amazon.mShop.android", "com.amazon.venezia" -> {

View File

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

View File

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

View File

@ -1,85 +1,14 @@
package de.mm20.launcher2.icons
import android.graphics.Color
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
import android.content.res.Resources
open class LauncherIcon(
foreground: Drawable,
background: Drawable? = null,
foregroundScale: Float = 1f,
backgroundScale: Float = 1f,
var autoGenerateBackgroundMode: Int = BACKGROUND_WHITE,
val isThemeable: Boolean = false,
) {
sealed interface LauncherIcon
var foreground = foreground
set(value) {
field = value
updateBackgroundColor()
notifyCallbacks()
}
data class StaticLauncherIcon(
val foregroundLayer: LauncherIconLayer,
val backgroundLayer: LauncherIconLayer,
): LauncherIcon
private fun updateBackgroundColor() {
if (background == null) {
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
}
interface DynamicLauncherIcon: LauncherIcon {
suspend fun getIcon(time: Long): StaticLauncherIcon
}

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.Typeface
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.LayerDrawable
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.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 hct.Hct
import palettes.TonalPalette
import scheme.Scheme
import java.text.SimpleDateFormat
import java.util.*
class CalendarEvent(
override val label: String,
@ -41,21 +32,14 @@ class CalendarEvent(
get() = "calendar://$id"
override fun getPlaceholderIcon(context: Context): LauncherIcon {
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val df = SimpleDateFormat("dd")
val s = (48 * context.dp).toInt()
val foreground = TextDrawable(
df.format(startTime),
color = Color.WHITE,
fontSize = 24 * context.dp,
typeface = Typeface.DEFAULT_BOLD,
height = s
)
val background = ColorDrawable(getDisplayColor())
return LauncherIcon(
foreground = foreground,
background = background,
foregroundScale = 0.74f
return StaticLauncherIcon(
foregroundLayer = TextLayer(
text = df.format(startTime),
color = Color.WHITE
),
backgroundLayer = ColorLayer(getDisplayColor())
)
}

View File

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

View File

@ -1,10 +1,12 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.graphics.Color
import androidx.core.content.ContextCompat
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.*
abstract class File(
@ -19,7 +21,7 @@ abstract class File(
open val providerIconRes: Int? = null
override fun getPlaceholderIcon(context: Context): LauncherIcon {
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val (resId, bgColor) = when {
isDirectory -> R.drawable.ic_file_folder to R.color.lightblue
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
}
}
return LauncherIcon(
foreground = context.getDrawable(resId)!!,
background = ColorDrawable(ContextCompat.getColor(context, bgColor)),
foregroundScale = 0.5f
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, resId)!!,
scale = 0.5f,
color = Color.WHITE
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, bgColor))
)
}
fun getFileType(context: Context): String {
if (isDirectory) return context.getString(R.string.file_type_directory)
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) {
"application/zip",
@ -107,7 +115,10 @@ abstract class File(
}
if (resource == R.string.file_type_none && label.matches(Regex(".+\\..+"))) {
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(resource)

View File

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

View File

@ -188,8 +188,6 @@
<string name="weather_condition_heavyrainandthunder">Starkregen und Gewitter</string>
<string name="weather_condition_wind">Wind</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_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>

View File

@ -173,8 +173,6 @@
<string name="shortcut_summary">Par %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="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 -->
<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>

View File

@ -291,7 +291,6 @@
<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_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_grid">Griglia</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_clockwidget">Orologio</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_clock_widget_style">Stile</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_about_telegram">Grupa Telegram</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_about_license">Ta aplikacja jest wolnym oprogramowaniem.</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_vertical">Pionowy</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_clockwidget_date_part">Data</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_owncloud_signin_summary">Autenticar para pesquisar no seu servidor Owncloud</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_about_license">Este aplicativo é um software livre.</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_about_telegram">Grup Telegram</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_about_license">Această aplicație este un software gratuit.</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_about_telegram">Telegram-grupp</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_about_license">Denna app är fri programvara.</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_about_telegram">Telegram群组</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_about_license_summary">根据 GNU 通用公共许可证 3.0 授权</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_about_telegram">Telegram group</string>
<string name="preference_about_fdroid">F-Droid repository</string>
<string name="preference_legacy_icon_bg">Icon background</string>
<string name="preference_legacy_icon_bg_summary">Legacy icon style</string>
<string name="preference_enforce_icon_shape">Enforce shape</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_about_license">This app is free software.</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 de.mm20.launcher2.database.entities.IconEntity
data class Icon(
data class IconPackIcon(
val type: String,
val componentName: ComponentName?,
val drawable: String?,

View File

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

View File

@ -6,6 +6,8 @@ import android.content.Intent
import android.content.IntentFilter
import android.util.LruCache
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.search.data.Searchable
import kotlinx.coroutines.CoroutineScope
@ -17,7 +19,6 @@ import kotlinx.coroutines.launch
class IconRepository(
val context: Context,
private val iconPackManager: IconPackManager,
private val dynamicIconController: DynamicIconController,
private val dataStore: LauncherDataStore
) {
@ -34,6 +35,11 @@ class IconRepository(
private var iconProviders: MutableStateFlow<List<IconProvider>> = MutableStateFlow(listOf())
private var placeholderProvider: IconProvider? = null
private var transformations: MutableStateFlow<List<LauncherIconTransformation>> =
MutableStateFlow(
listOf()
)
init {
requestIconPackListUpdate()
context.registerReceiver(appReceiver, IntentFilter().apply {
@ -62,19 +68,23 @@ class IconRepository(
providers.add(
IconPackIconProvider(
context,
settings.iconPack,
settings.legacyIconBg
settings.iconPack
)
)
}
providers.add(GoogleClockIconProvider(context))
providers.add(CalendarIconProvider(context))
providers.add(SystemIconProvider(context, settings.legacyIconBg))
providers.add(SystemIconProvider(context))
providers.add(placeholderProvider)
cache.evictAll()
val transformations = mutableListOf<LauncherIconTransformation>()
if (settings.adaptify) transformations.add(LegacyToAdaptiveTransformation())
this@IconRepository.placeholderProvider = placeholderProvider
iconProviders.value = providers
this@IconRepository.transformations.value = transformations
}
}
}
@ -82,28 +92,33 @@ class IconRepository(
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = channelFlow {
iconProviders.collectLatest { providers ->
var icon = cache.get(searchable.key)
if (icon != null) {
send(icon)
return@collectLatest
}
val placeholder = placeholderProvider?.getIcon(searchable, size)
placeholder?.let { send(it) }
for (provider in providers) {
val ic = provider.getIcon(searchable, size)
if (ic != null) {
if (ic is DynamicLauncherIcon) {
dynamicIconController.registerIcon(ic)
}
icon = ic
break
transformations.collectLatest { transformations ->
var icon = cache.get(searchable.key)
if (icon != null) {
send(icon)
return@collectLatest
}
val placeholder = placeholderProvider?.getIcon(searchable, size)
placeholder?.let { send(it) }
for (provider in providers) {
val ic = provider.getIcon(searchable, size)
if (ic != null) {
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
val iconsModule = module {
single { DynamicIconController(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.pm.PackageManager
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.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application
@ -35,13 +35,9 @@ class CalendarIconProvider(val context: Context): IconProvider {
drawableIds[i] = typedArray.getResourceId(i, 0)
}
typedArray.recycle()
return CalendarDynamicLauncherIcon(
foreground = ColorDrawable(0),
background = ColorDrawable(0),
foregroundScale = 1.5f,
backgroundScale = 1.5f,
packageName = component.packageName,
drawableIds = drawableIds
return DynamicCalendarIcon(
resources = resources,
resourceIds = drawableIds
)
}
}

View File

@ -5,12 +5,12 @@ import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.graphics.drawable.RotateDrawable
import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.icons.ClockDynamicLauncherIcon
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
import kotlin.math.roundToInt
class GoogleClockIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
@ -26,7 +26,7 @@ class GoogleClockIconProvider(val context: Context) : IconProvider {
return null
}
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 baseIcon = try {
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 hourLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX")
appInfo.metaData.getInt("com.android.launcher3.HOUR_LAYER_INDEX")
val minuteLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX")
appInfo.metaData.getInt("com.android.launcher3.MINUTE_LAYER_INDEX")
val secondLayer =
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX")
return ClockDynamicLauncherIcon(
foreground = foreground,
background = baseIcon.background,
foregroundScale = 1.5f,
backgroundScale = 1.5f,
hourLayer = hourLayer,
minuteLayer = minuteLayer,
secondLayer = secondLayer
appInfo.metaData.getInt("com.android.launcher3.SECOND_LAYER_INDEX")
val defaultHour =
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_HOUR")
val defaultMinute =
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_MINUTE")
val defaultSecond =
appInfo.metaData.getInt("com.android.launcher3.DEFAULT_SECOND")
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.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.util.Log
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.CalendarDynamicLauncherIcon
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.randomElementOrNull
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.search.data.LauncherApp
@ -24,9 +22,8 @@ import kotlin.math.roundToInt
class IconPackIconProvider(
private val context: Context,
private val iconPack: String,
private val legacyIconBackground: Settings.IconSettings.LegacyIconBackground
): IconProvider {
private val iconPack: String
): IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
if (searchable !is LauncherApp) return null
val res = try {
@ -49,25 +46,31 @@ class IconPackIconProvider(
val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null
return when {
drawable is AdaptiveIconDrawable -> {
LauncherIcon(
foreground = drawable.foreground,
background = drawable.background,
foregroundScale = 1.5f,
backgroundScale = 1.5f
StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = drawable.foreground,
scale = 1.5f
),
backgroundLayer = StaticIconLayer(
icon = drawable.background,
scale = 1.5f
)
)
}
else -> {
LauncherIcon(
foreground = drawable,
foregroundScale = getScale(),
autoGenerateBackgroundMode = legacyIconBackground.number
StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = drawable,
scale = getScale()
),
backgroundLayer = TransparentLayer
)
}
}
}
private fun getScale(): Float {
return 0.7f
return 1f
}
private suspend fun generateIcon(
@ -147,10 +150,12 @@ class IconPackIconProvider(
}
}
return LauncherIcon(
foreground = BitmapDrawable(context.resources, bitmap),
foregroundScale = getScale(),
autoGenerateBackgroundMode = legacyIconBackground.number
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = BitmapDrawable(context.resources, bitmap),
scale = getScale(),
),
backgroundLayer = TransparentLayer
)
}
@ -180,7 +185,7 @@ class IconPackIconProvider(
private fun getIconPackCalendarIcon(
context: Context,
baseIconName: String
): CalendarDynamicLauncherIcon? {
): DynamicCalendarIcon? {
val resources = try {
context.packageManager.getResourcesForApplication(iconPack)
} catch (e: PackageManager.NameNotFoundException) {
@ -192,13 +197,9 @@ class IconPackIconProvider(
if (id == 0) return null
id
}.toIntArray()
return CalendarDynamicLauncherIcon(
foreground = ColorDrawable(0),
background = ColorDrawable(0),
foregroundScale = 1.5f,
backgroundScale = 1.5f,
packageName = iconPack,
drawableIds = drawableIds,
return DynamicCalendarIcon(
resources = resources,
resourceIds = drawableIds
)
}
}

View File

@ -5,7 +5,7 @@ import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
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)
}
}

View File

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

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.icons.providers
import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.data.Searchable
internal class ThemedPlaceholderIconProvider(
@ -11,13 +11,30 @@ internal class ThemedPlaceholderIconProvider(
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
val icon = searchable.getPlaceholderIcon(context)
return LauncherIcon(
foreground = icon.foreground,
foregroundScale = icon.foregroundScale,
background = icon.background,
backgroundScale = icon.backgroundScale,
isThemeable = true
return StaticLauncherIcon(
foregroundLayer = asThemed(icon.foregroundLayer),
backgroundLayer = asThemed(icon.backgroundLayer),
)
}
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
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.annotation.Px
@ -13,4 +15,11 @@ fun Drawable.toBitmapOrNull(
): Bitmap? {
if (this is BitmapDrawable && bitmap == null) return null
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 {
CrashReporter.logException(it)
Log.d("MM20", "corruptionHandler")
Settings.getDefaultInstance()
}
)
internal const val SchemaVersion = 6
internal const val SchemaVersion = 7
internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
return listOf(
@ -34,5 +33,6 @@ internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
Migration_3_4(),
Migration_4_5(),
Migration_5_6(),
Migration_6_7(),
)
}

View File

@ -128,7 +128,7 @@ fun createFactorySettings(context: Context): Settings {
)
.setIcons(
Settings.IconSettings.newBuilder()
.setLegacyIconBg(Settings.IconSettings.LegacyIconBackground.Dynamic)
.setAdaptify(true)
.setShape(Settings.IconSettings.IconShape.PlatformDefault)
.setThemedIcons(false)
.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;
bool themed_icons = 2;
string icon_pack = 3;
enum LegacyIconBackground {
Dynamic = 0;
None = 1;
White = 2;
}
LegacyIconBackground legacyIconBg = 4;
bool adaptify = 5;
}
IconSettings icons = 21;

View File

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

View File

@ -1,14 +1,11 @@
package de.mm20.launcher2.ui.component
import android.graphics.*
import android.graphics.Matrix
import android.graphics.Path
import android.graphics.RectF
import android.graphics.drawable.AdaptiveIconDrawable
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@ -25,15 +22,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color
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.res.painterResource
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.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.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.roundToInt
@ -48,6 +56,25 @@ fun ShapedLauncherIcon(
onLongClick: (() -> Unit)? = null,
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(
modifier = modifier
.size(size)
@ -56,7 +83,7 @@ fun ShapedLauncherIcon(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
clip = true
clip = currentIcon?.backgroundLayer !is TransparentLayer
this.shape = shape
}
.combinedClickable(
@ -68,45 +95,19 @@ fun ShapedLauncherIcon(
),
contentAlignment = Alignment.Center
) {
if (icon != null) {
val fgScale = icon.foregroundScale
val bgScale = icon.backgroundScale
val themedFgColor = MaterialTheme.colorScheme.onPrimaryContainer
val themedBgColor = MaterialTheme.colorScheme.primaryContainer
val fg = remember(icon, icon.isThemeable, themedFgColor) {
icon.foreground.also {
if (icon.isThemeable) it.setTint(themedFgColor.toArgb())
}
}
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)
}
}
currentIcon?.let {
IconLayer(
it.backgroundLayer,
size,
colorTone = if (!LocalDarkTheme.current) 30 else 90,
MaterialTheme.colorScheme.primaryContainer
)
IconLayer(
it.foregroundLayer,
size,
colorTone = if (!LocalDarkTheme.current) 90 else 10,
MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
if (badge != null) {
@ -138,12 +139,18 @@ fun ShapedLauncherIcon(
val number = badge.number
if (badgeIconRes != null) {
Image(
modifier = Modifier.fillMaxSize().padding(size / 48),
modifier = Modifier
.fillMaxSize()
.padding(size / 48),
painter = painterResource(badgeIconRes),
contentDescription = null
)
} else if (badgeIcon != null) {
Canvas(modifier = Modifier.fillMaxSize().padding(size / 48)) {
Canvas(
modifier = Modifier
.fillMaxSize()
.padding(size / 48)
) {
badgeIcon.setBounds(
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 }
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.android.launcher3.GestureNavContract
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.icons.DynamicIconController
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.R
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.LocalWindowSize
import de.mm20.launcher2.ui.theme.LauncherTheme
import org.koin.android.ext.android.inject
class LauncherActivity : BaseActivity() {
@ -174,10 +172,6 @@ class LauncherActivity : BaseActivity() {
editFavoritesDialog = null
}
}
val dynamicIconController: DynamicIconController by inject()
lifecycle.addObserver(dynamicIconController)
}
override fun onAttachedToWindow() {

View File

@ -1,31 +1,10 @@
package de.mm20.launcher2.ui.legacy.view
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.graphics.drawable.AdaptiveIconDrawable
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
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.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.get
import java.lang.Math.pow
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.roundToInt
class LauncherIconView : View, KoinComponent {
constructor(context: Context) : super(context)
@ -36,499 +15,10 @@ class LauncherIconView : View, KoinComponent {
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
set(value) {
field = value
foregroundScale = value?.foregroundScale ?: 1f
backgroundScale = value?.backgroundScale ?: 1f
value?.registerCallback(iconObserver)
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
import android.graphics.drawable.ColorDrawable
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.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.airbnb.lottie.LottieProperty
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.HorizontalPagerIndicator
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.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
@ -58,8 +56,8 @@ fun AppearanceSettingsScreen() {
title = stringResource(id = R.string.preference_layout),
summary = stringResource(id = R.string.preference_layout_summary),
value = layout, onValueChanged = {
viewModel.setLayout(it)
})
viewModel.setLayout(it)
})
val theme by viewModel.theme.observeAsState()
ListPreference(
title = stringResource(id = R.string.preference_theme),
@ -147,6 +145,15 @@ fun AppearanceSettingsScreen() {
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()
SwitchPreference(
title = stringResource(R.string.preference_themed_icons),
@ -176,17 +183,6 @@ fun AppearanceSettingsScreen() {
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)) {
val searchBarStyle by viewModel.searchBarStyle.observeAsState()
@ -370,13 +366,15 @@ fun IconShapePreference(
) {
ShapedLauncherIcon(
size = 48.dp,
icon = LauncherIcon(
foreground = AppCompatResources.getDrawable(
LocalContext.current,
R.mipmap.ic_launcher_foreground
)!!,
foregroundScale = 1.5f,
background = ColorDrawable(LocalContext.current.getColor(R.color.ic_launcher_background))
icon = StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = ContextCompat.getDrawable(
LocalContext.current,
R.mipmap.ic_launcher_foreground
)!!,
scale = 1.5f,
),
ColorLayer(LocalContext.current.getColor(R.color.ic_launcher_background))
),
onClick = {
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)
@Composable
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.AppearanceSettings.ColorScheme
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import de.mm20.launcher2.preferences.Settings.SearchBarSettings
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@ -108,14 +107,14 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
}
}
val legacyIconBackground = dataStore.data.map { it.icons.legacyIconBg }.asLiveData()
fun setLegacyIconBackground(legacyIconBackground: LegacyIconBackground) {
val adaptifyLegacyIcons = dataStore.data.map { it.icons.adaptify }.asLiveData()
fun setAdaptifyLegacyIcons(adaptify: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setIcons(
it.icons.toBuilder()
.setLegacyIconBg(legacyIconBackground)
.setAdaptify(adaptify)
)
.build()
}

View File

@ -3,22 +3,13 @@ package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette
import androidx.core.content.ContextCompat
import coil.imageLoader
import coil.request.ImageRequest
import de.mm20.launcher2.graphics.TextDrawable
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.icons.*
import de.mm20.launcher2.websites.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutionException
class Website(
@ -31,7 +22,10 @@ class Website(
) : Searchable() {
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
try {
val request = ImageRequest.Builder(context)
@ -40,11 +34,13 @@ class Website(
.allowHardware(false)
.build()
val icon = context.imageLoader.execute(request).drawable ?: return null
return LauncherIcon(
foreground = icon,
background = color.let { ColorDrawable(it) },
foregroundScale = 0.7f,
autoGenerateBackgroundMode = legacyIconBackground.number
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = icon,
scale = 0.7f,
),
backgroundLayer = ColorLayer(color)
)
} catch (e: ExecutionException) {
return null
@ -52,19 +48,20 @@ class Website(
}
override fun getPlaceholderIcon(context: Context): LauncherIcon {
val drawable = if (label.isNotEmpty()) {
TextDrawable(
label[0].toString(),
typeface = Typeface.DEFAULT_BOLD,
fontSize = 40 * context.sp,
height = (48 * context.dp).toInt()
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
if (label.isNotBlank()) {
return StaticLauncherIcon(
foregroundLayer = TextLayer(text = label[0].toString(), color = Color.WHITE),
backgroundLayer = ColorLayer(Color.LTGRAY)
)
} else context.getDrawable(R.drawable.ic_website)!!
return LauncherIcon(
foreground = drawable,
background = ColorDrawable(if (color != 0) color else Color.LTGRAY),
foregroundScale = 1f
}
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
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.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.text.Spanned
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.text.HtmlCompat
import androidx.core.text.toHtml
import androidx.core.content.ContextCompat
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.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(
override val label: String,
val id: Long,
val text: String,
val image: String?,
val wikipediaUrl: String,
override val label: String,
val id: Long,
val text: String,
val image: String?,
val wikipediaUrl: String,
) : Searchable() {
override val key = "wikipedia://$wikipediaUrl:$id"
override fun getPlaceholderIcon(context: Context): LauncherIcon {
return LauncherIcon(
foreground = context.getDrawable(R.drawable.ic_wikipedia)!!,
background = ColorDrawable(0xFFF0F0F0.toInt())
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_wikipedia)!!,
scale = 1f
),
backgroundLayer = ColorLayer(0xFFF0F0F0.toInt())
)
}
override fun getLaunchIntent(context: Context): Intent? {
val intent = CustomTabsIntent
.Builder()
.setToolbarColor(Color.BLACK)
.enableUrlBarHiding()
.setShowTitle(true)
.build()
.Builder()
.setToolbarColor(Color.BLACK)
.enableUrlBarHiding()
.setShowTitle(true)
.build()
val uri = "${wikipediaUrl}/wiki?curid=$id"
intent.intent.data = Uri.parse(uri)
return intent.intent