(feat) custom typography schemes

This commit is contained in:
MM20
2025-06-18 22:28:13 +02:00
parent 4bf9ee8b0c
commit da8416a58c
31 changed files with 2856 additions and 117 deletions
@@ -10,6 +10,7 @@ import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import de.mm20.launcher2.database.daos.PluginDao
import de.mm20.launcher2.database.daos.ThemeDao
import de.mm20.launcher2.database.entities.ColorsEntity
import de.mm20.launcher2.database.entities.CurrencyEntity
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.database.entities.ForecastEntity
@@ -18,9 +19,9 @@ import de.mm20.launcher2.database.entities.IconPackEntity
import de.mm20.launcher2.database.entities.PluginEntity
import de.mm20.launcher2.database.entities.SavedSearchableEntity
import de.mm20.launcher2.database.entities.SearchActionEntity
import de.mm20.launcher2.database.entities.ColorsEntity
import de.mm20.launcher2.database.entities.ShapesEntity
import de.mm20.launcher2.database.entities.TransparenciesEntity
import de.mm20.launcher2.database.entities.TypographyEntity
import de.mm20.launcher2.database.entities.WidgetEntity
import de.mm20.launcher2.database.migrations.Migration_10_11
import de.mm20.launcher2.database.migrations.Migration_11_12
@@ -41,6 +42,7 @@ import de.mm20.launcher2.database.migrations.Migration_25_26
import de.mm20.launcher2.database.migrations.Migration_26_27
import de.mm20.launcher2.database.migrations.Migration_27_28
import de.mm20.launcher2.database.migrations.Migration_28_29
import de.mm20.launcher2.database.migrations.Migration_29_30
import de.mm20.launcher2.database.migrations.Migration_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9
@@ -62,7 +64,8 @@ import java.util.UUID
PluginEntity::class,
ShapesEntity::class,
TransparenciesEntity::class,
], version = 29, exportSchema = true
TypographyEntity::class,
], version = 30, exportSchema = true
)
@TypeConverters(ComponentNameConverter::class)
abstract class AppDatabase : RoomDatabase() {
@@ -164,6 +167,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_26_27(),
Migration_27_28(),
Migration_28_29(),
Migration_29_30(),
).build()
if (_instance == null) _instance = instance
return instance
@@ -7,6 +7,7 @@ import androidx.room.Update
import de.mm20.launcher2.database.entities.ColorsEntity
import de.mm20.launcher2.database.entities.ShapesEntity
import de.mm20.launcher2.database.entities.TransparenciesEntity
import de.mm20.launcher2.database.entities.TypographyEntity
import kotlinx.coroutines.flow.Flow
import java.util.UUID
@@ -21,6 +22,9 @@ interface ThemeDao {
@Query("SELECT * FROM Transparencies")
fun getAllTransparencies(): Flow<List<TransparenciesEntity>>
@Query("SELECT * FROM Typography")
fun getAllTypographies(): Flow<List<TypographyEntity>>
@Query("SELECT * FROM Theme WHERE id = :id LIMIT 1")
fun getColors(id: UUID): Flow<ColorsEntity?>
@@ -30,6 +34,9 @@ interface ThemeDao {
@Query("SELECT * FROM Transparencies WHERE id = :id LIMIT 1")
fun getTransparencies(id: UUID): Flow<TransparenciesEntity?>
@Query("SELECT * FROM Typography WHERE id = :id LIMIT 1")
fun getTypography(id: UUID): Flow<TypographyEntity?>
@Insert
suspend fun insertColors(colors: ColorsEntity)
@@ -39,6 +46,9 @@ interface ThemeDao {
@Insert
suspend fun insertTransparencies(transparencies: TransparenciesEntity)
@Insert
suspend fun insertTypography(typography: TypographyEntity)
@Update
suspend fun updateColors(colors: ColorsEntity)
@@ -48,6 +58,9 @@ interface ThemeDao {
@Update
suspend fun updateTransparencies(transparencies: TransparenciesEntity)
@Update
suspend fun updateTypography(typography: TypographyEntity)
@Query("DELETE FROM Theme WHERE id = :id")
suspend fun deleteColors(id: UUID)
@@ -57,6 +70,9 @@ interface ThemeDao {
@Query("DELETE FROM Transparencies WHERE id = :id")
suspend fun deleteTransparencies(id: UUID)
@Query("DELETE FROM Typography WHERE id = :id")
suspend fun deleteTypography(id: UUID)
@Query("DELETE FROM Theme")
suspend fun deleteAllColors()
@@ -66,6 +82,9 @@ interface ThemeDao {
@Query("DELETE FROM Transparencies")
suspend fun deleteAllTransparencies()
@Query("DELETE FROM Typography")
suspend fun deleteAllTypographies()
@Insert
fun insertAllColors(colors: List<ColorsEntity>)
@@ -74,4 +93,7 @@ interface ThemeDao {
@Insert
fun insertAllTransparencies(transparencies: List<TransparenciesEntity>)
@Insert
fun insertAllTypographies(typography: List<TypographyEntity>)
}
@@ -0,0 +1,43 @@
package de.mm20.launcher2.database.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "Typography")
data class TypographyEntity(
@PrimaryKey val id: UUID,
val name: String,
val fonts: String? = null,
val displayLarge: String? = null,
val displayMedium: String? = null,
val displaySmall: String? = null,
val headlineLarge: String? = null,
val headlineMedium: String? = null,
val headlineSmall: String? = null,
val titleLarge: String? = null,
val titleMedium: String? = null,
val titleSmall: String? = null,
val bodyLarge: String? = null,
val bodyMedium: String? = null,
val bodySmall: String? = null,
val labelLarge: String? = null,
val labelMedium: String? = null,
val labelSmall: String? = null,
val emphasizedDisplayLarge: String? = null,
val emphasizedDisplayMedium: String? = null,
val emphasizedDisplaySmall: String? = null,
val emphasizedHeadlineLarge: String? = null,
val emphasizedHeadlineMedium: String? = null,
val emphasizedHeadlineSmall: String? = null,
val emphasizedTitleLarge: String? = null,
val emphasizedTitleMedium: String? = null,
val emphasizedTitleSmall: String? = null,
val emphasizedBodyLarge: String? = null,
val emphasizedBodyMedium: String? = null,
val emphasizedBodySmall: String? = null,
val emphasizedLabelLarge: String? = null,
val emphasizedLabelMedium: String? = null,
val emphasizedLabelSmall: String? = null,
)
@@ -0,0 +1,50 @@
package de.mm20.launcher2.database.migrations
import androidx.room.migration.Migration
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.execSQL
class Migration_29_30 : Migration(29, 30) {
override fun migrate(connection: SQLiteConnection) {
connection.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Typography` (
`id` BLOB NOT NULL PRIMARY KEY,
`name` TEXT NOT NULL,
`fonts` TEXT,
`displayLarge` TEXT,
`displayMedium` TEXT,
`displaySmall` TEXT,
`headlineLarge` TEXT,
`headlineMedium` TEXT,
`headlineSmall` TEXT,
`titleLarge` TEXT,
`titleMedium` TEXT,
`titleSmall` TEXT,
`bodyLarge` TEXT,
`bodyMedium` TEXT,
`bodySmall` TEXT,
`labelLarge` TEXT,
`labelMedium` TEXT,
`labelSmall` TEXT,
`emphasizedDisplayLarge` TEXT,
`emphasizedDisplayMedium` TEXT,
`emphasizedDisplaySmall` TEXT,
`emphasizedHeadlineLarge` TEXT,
`emphasizedHeadlineMedium` TEXT,
`emphasizedHeadlineSmall` TEXT,
`emphasizedTitleLarge` TEXT,
`emphasizedTitleMedium` TEXT,
`emphasizedTitleSmall` TEXT,
`emphasizedBodyLarge` TEXT,
`emphasizedBodyMedium` TEXT,
`emphasizedBodySmall` TEXT,
`emphasizedLabelLarge` TEXT,
`emphasizedLabelMedium` TEXT,
`emphasizedLabelSmall` TEXT
)
""".trimIndent()
)
}
}
@@ -1,10 +1,5 @@
package de.mm20.launcher2.themes
import de.mm20.launcher2.themes.colors.Color
import de.mm20.launcher2.themes.colors.ColorRef
import de.mm20.launcher2.themes.colors.ColorScheme
import de.mm20.launcher2.themes.colors.CorePaletteColor
import de.mm20.launcher2.themes.colors.StaticColor
import java.util.UUID
@@ -17,4 +12,8 @@ val ExtraRoundShapesId = UUID(0L, 1L)
val CutShapesId = UUID(0L, 2L)
val RectShapesId = UUID(0L, 3L)
val SemiTransparentId = UUID(0L, 1L)
val SemiTransparentId = UUID(0L, 1L)
val SystemFontId = UUID(0L, 1L)
val MonospaceId = UUID(0L, 2L)
val SerifId = UUID(0L, 3L)
@@ -3,6 +3,7 @@ package de.mm20.launcher2.themes
import de.mm20.launcher2.themes.colors.Color
import de.mm20.launcher2.themes.colors.StaticColor
import de.mm20.launcher2.themes.shapes.Shape
import de.mm20.launcher2.themes.typography.FontWeight
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@@ -26,8 +27,9 @@ val ThemeJson = Json {
coerceInputValues = true
}
internal object ColorSerializer: KSerializer<Color> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING)
internal object ColorSerializer : KSerializer<Color> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING)
override fun serialize(
encoder: Encoder,
@@ -42,8 +44,9 @@ internal object ColorSerializer: KSerializer<Color> {
}
}
internal object ShapeSerializer: KSerializer<Shape> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ShapeSerializer", PrimitiveKind.STRING)
internal object ShapeSerializer : KSerializer<Shape> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("ShapeSerializer", PrimitiveKind.STRING)
override fun serialize(
encoder: Encoder,
@@ -55,4 +58,34 @@ internal object ShapeSerializer: KSerializer<Shape> {
override fun deserialize(decoder: Decoder): Shape {
return Shape.fromString(decoder.decodeString())!!
}
}
internal object FontWeightSerializer : KSerializer<FontWeight?> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("FontWeightSerializer", PrimitiveKind.STRING)
override fun serialize(
encoder: Encoder,
value: FontWeight?
) {
if (value is FontWeight.Absolute) {
encoder.encodeString(value.weight.toString())
} else if (value is FontWeight.Relative) {
encoder.encodeString(
(if (value.relativeWeight >= 0) "+" else "-") + value.relativeWeight.toString()
)
}
}
override fun deserialize(decoder: Decoder): FontWeight? {
val str = decoder.decodeString()
if (str.isBlank()) return null
return when {
str.startsWith("+") -> FontWeight.Relative(str.substring(1).toInt())
str.startsWith("-") -> FontWeight.Relative(-str.substring(1).toInt())
else -> FontWeight.Absolute(str.toInt())
}
}
}
@@ -8,6 +8,7 @@ import de.mm20.launcher2.themes.colors.Colors
import de.mm20.launcher2.themes.colors.ColorsRepository
import de.mm20.launcher2.themes.shapes.ShapesRepository
import de.mm20.launcher2.themes.transparencies.TransparenciesRepository
import de.mm20.launcher2.themes.typography.TypographyRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
@@ -21,6 +22,7 @@ class ThemeRepository(
val colors = ColorsRepository(context, database)
val shapes = ShapesRepository(context, database)
val transparencies = TransparenciesRepository(context, database)
val typographies = TypographyRepository(context, database)
override suspend fun backup(toDir: File) = withContext(Dispatchers.IO) {
@@ -0,0 +1,127 @@
package de.mm20.launcher2.themes.typography
val DefaultTextStyles = TextStyles(
displayLarge = TextStyle(
fontFamily = "brand",
fontSize = 57,
fontWeight = FontWeight.Absolute(500),
lineHeight = 64,
letterSpacing = -0.25f
),
displayMedium = TextStyle(
fontFamily = "brand",
fontSize = 45,
fontWeight = FontWeight.Absolute(500),
lineHeight = 52,
letterSpacing = 0f
),
displaySmall = TextStyle(
fontFamily = "brand",
fontSize = 36,
fontWeight = FontWeight.Absolute(500),
lineHeight = 44,
letterSpacing = 0f
),
headlineLarge = TextStyle(
fontFamily = "brand",
fontSize = 32,
fontWeight = FontWeight.Absolute(600),
lineHeight = 40,
letterSpacing = 0f
),
headlineMedium = TextStyle(
fontFamily = "brand",
fontSize = 28,
fontWeight = FontWeight.Absolute(600),
lineHeight = 36,
letterSpacing = 0f
),
headlineSmall = TextStyle(
fontFamily = "brand",
fontSize = 24,
fontWeight = FontWeight.Absolute(600),
lineHeight = 32,
letterSpacing = 0f
),
titleLarge = TextStyle(
fontFamily = "brand",
fontSize = 22,
fontWeight = FontWeight.Absolute(600),
lineHeight = 28,
letterSpacing = 0f
),
titleMedium = TextStyle(
fontFamily = "brand",
fontSize = 16,
fontWeight = FontWeight.Absolute(600),
lineHeight = 24,
letterSpacing = 0.15f
),
titleSmall = TextStyle(
fontFamily = "brand",
fontSize = 14,
fontWeight = FontWeight.Absolute(600),
lineHeight = 20,
letterSpacing = 0.1f
),
bodyLarge = TextStyle(
fontFamily = "plain",
fontSize = 16,
fontWeight = FontWeight.Absolute(400),
lineHeight = 24,
letterSpacing = 0.5f
),
bodyMedium = TextStyle(
fontFamily = "plain",
fontSize = 14,
fontWeight = FontWeight.Absolute(400),
lineHeight = 20,
letterSpacing = 0.25f
),
bodySmall = TextStyle(
fontFamily = "plain",
fontSize = 12,
fontWeight = FontWeight.Absolute(400),
lineHeight = 16,
letterSpacing = 0.4f
),
labelLarge = TextStyle(
fontFamily = "brand",
fontSize = 14,
fontWeight = FontWeight.Absolute(500),
lineHeight = 20,
letterSpacing = 0.1f
),
labelMedium = TextStyle(
fontFamily = "brand",
fontSize = 12,
fontWeight = FontWeight.Absolute(500),
lineHeight = 16,
letterSpacing = 0.5f
),
labelSmall = TextStyle(
fontFamily = "brand",
fontSize = 11,
fontWeight = FontWeight.Absolute(500),
lineHeight = 16,
letterSpacing = 0.5f
)
)
val DefaultEmphasizedTextStyles = TextStyles(
displayLarge = TextStyle(fontWeight = FontWeight.Relative(100)),
displayMedium = TextStyle(fontWeight = FontWeight.Relative(100)),
displaySmall = TextStyle(fontWeight = FontWeight.Relative(100)),
headlineLarge = TextStyle(fontWeight = FontWeight.Relative(100)),
headlineMedium = TextStyle(fontWeight = FontWeight.Relative(100)),
headlineSmall = TextStyle(fontWeight = FontWeight.Relative(100)),
titleLarge = TextStyle(fontWeight = FontWeight.Relative(100)),
titleMedium = TextStyle(fontWeight = FontWeight.Relative(100)),
titleSmall = TextStyle(fontWeight = FontWeight.Relative(100)),
bodyLarge = TextStyle(fontWeight = FontWeight.Relative(100)),
bodyMedium = TextStyle(fontWeight = FontWeight.Relative(100)),
bodySmall = TextStyle(fontWeight = FontWeight.Relative(100)),
labelLarge = TextStyle(fontWeight = FontWeight.Relative(200)),
labelMedium = TextStyle(fontWeight = FontWeight.Relative(200)),
labelSmall = TextStyle(fontWeight = FontWeight.Relative(200))
)
@@ -0,0 +1,45 @@
package de.mm20.launcher2.themes.typography
import android.content.Context
import android.graphics.Typeface
import android.graphics.fonts.SystemFonts
import android.os.Build
import android.util.Log
import androidx.core.graphics.TypefaceCompat
data class FontList(
val builtIn: List<FontFamily>,
val generic: List<FontFamily>,
val deviceDefault: List<FontFamily>,
val system: List<FontFamily>,
)
class FontManager(
private val context: Context
) {
fun getInstalledFonts(): FontList {
val deviceHeadlineResId = context.resources
.getIdentifier("config_headlineFontFamily", "string", "android")
val deviceBodyResId = context.resources
.getIdentifier("config_bodyFontFamily", "string", "android")
val deviceHeadlineExists = deviceHeadlineResId != 0 &&
context.resources.getString(deviceHeadlineResId).isNotBlank()
val deviceBodyExists = deviceBodyResId != 0 &&
context.resources.getString(deviceBodyResId).isNotBlank()
return FontList(
builtIn = listOf(FontFamily.LauncherDefault),
generic = listOf(
FontFamily.SansSerif,
FontFamily.Serif,
FontFamily.Monospace,
),
deviceDefault = listOfNotNull(
if (deviceHeadlineExists) FontFamily.DeviceHeadline else null,
if (deviceBodyExists) FontFamily.DeviceBody else null,
),
system = emptyList(),
)
}
}
@@ -0,0 +1,228 @@
package de.mm20.launcher2.themes.typography
import de.mm20.launcher2.database.entities.TypographyEntity
import de.mm20.launcher2.serialization.UUIDSerializer
import de.mm20.launcher2.themes.FontWeightSerializer
import de.mm20.launcher2.themes.ThemeJson
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.UUID
@Serializable
data class Typography(
@Serializable(with = UUIDSerializer::class) val id: UUID = UUID.randomUUID(),
val builtIn: Boolean = false,
val name: String,
/**
* Map of font families used in this typography.
* `null` refers to the default font (sans-serif).
*/
val fonts: Map<String, FontFamily?> = mapOf("brand" to null, "plain" to null),
val styles: TextStyles<@Contextual FontWeight.Absolute?> = TextStyles(),
val emphasizedStyles: TextStyles<FontWeight?> = TextStyles(),
) {
internal constructor(entity: TypographyEntity) : this(
id = entity.id,
builtIn = false,
name = entity.name,
fonts = entity.fonts?.let { ThemeJson.decodeFromString(it) } ?: mapOf(
"brand" to null,
"plain" to null
),
styles = TextStyles(
bodySmall = TextStyle.fromString(entity.bodySmall),
bodyMedium = TextStyle.fromString(entity.bodyMedium),
bodyLarge = TextStyle.fromString(entity.bodyLarge),
labelSmall = TextStyle.fromString(entity.labelSmall),
labelMedium = TextStyle.fromString(entity.labelMedium),
labelLarge = TextStyle.fromString(entity.labelLarge),
titleSmall = TextStyle.fromString(entity.titleSmall),
titleMedium = TextStyle.fromString(entity.titleMedium),
titleLarge = TextStyle.fromString(entity.titleLarge),
headlineSmall = TextStyle.fromString(entity.headlineSmall),
headlineMedium = TextStyle.fromString(entity.headlineMedium),
headlineLarge = TextStyle.fromString(entity.headlineLarge),
displaySmall = TextStyle.fromString(entity.displaySmall),
displayMedium = TextStyle.fromString(entity.displayMedium),
displayLarge = TextStyle.fromString(entity.displayLarge)
),
emphasizedStyles = TextStyles(
bodySmall = TextStyle.fromString(entity.emphasizedBodySmall),
bodyMedium = TextStyle.fromString(entity.emphasizedBodyMedium),
bodyLarge = TextStyle.fromString(entity.emphasizedBodyLarge),
labelSmall = TextStyle.fromString(entity.emphasizedLabelSmall),
labelMedium = TextStyle.fromString(entity.emphasizedLabelMedium),
labelLarge = TextStyle.fromString(entity.emphasizedLabelLarge),
titleSmall = TextStyle.fromString(entity.emphasizedTitleSmall),
titleMedium = TextStyle.fromString(entity.emphasizedTitleMedium),
titleLarge = TextStyle.fromString(entity.emphasizedTitleLarge),
headlineSmall = TextStyle.fromString(entity.emphasizedHeadlineSmall),
headlineMedium = TextStyle.fromString(entity.emphasizedHeadlineMedium),
headlineLarge = TextStyle.fromString(entity.emphasizedHeadlineLarge),
displaySmall = TextStyle.fromString(entity.emphasizedDisplaySmall),
displayMedium = TextStyle.fromString(entity.emphasizedDisplayMedium),
displayLarge = TextStyle.fromString(entity.emphasizedDisplayLarge)
)
)
internal fun toEntity(): TypographyEntity {
return TypographyEntity(
id = id,
name = name,
fonts = ThemeJson.encodeToString(fonts),
displayLarge = styles.displayLarge?.toString(),
displayMedium = styles.displayMedium?.toString(),
displaySmall = styles.displaySmall?.toString(),
headlineLarge = styles.headlineLarge?.toString(),
headlineMedium = styles.headlineMedium?.toString(),
headlineSmall = styles.headlineSmall?.toString(),
titleLarge = styles.titleLarge?.toString(),
titleMedium = styles.titleMedium?.toString(),
titleSmall = styles.titleSmall?.toString(),
bodyLarge = styles.bodyLarge?.toString(),
bodyMedium = styles.bodyMedium?.toString(),
bodySmall = styles.bodySmall?.toString(),
labelLarge = styles.labelLarge?.toString(),
labelMedium = styles.labelMedium?.toString(),
labelSmall = styles.labelSmall?.toString(),
emphasizedDisplayLarge = emphasizedStyles.displayLarge?.toString(),
emphasizedDisplayMedium = emphasizedStyles.displayMedium?.toString(),
emphasizedDisplaySmall = emphasizedStyles.displaySmall?.toString(),
emphasizedHeadlineLarge = emphasizedStyles.headlineLarge?.toString(),
emphasizedHeadlineMedium = emphasizedStyles.headlineMedium?.toString(),
emphasizedHeadlineSmall = emphasizedStyles.headlineSmall?.toString(),
emphasizedTitleLarge = emphasizedStyles.titleLarge?.toString(),
emphasizedTitleMedium = emphasizedStyles.titleMedium?.toString(),
emphasizedTitleSmall = emphasizedStyles.titleSmall?.toString(),
emphasizedBodyLarge = emphasizedStyles.bodyLarge?.toString(),
emphasizedBodyMedium = emphasizedStyles.bodyMedium?.toString(),
emphasizedBodySmall = emphasizedStyles.bodySmall?.toString(),
emphasizedLabelLarge = emphasizedStyles.labelLarge?.toString(),
emphasizedLabelMedium = emphasizedStyles.labelMedium?.toString(),
emphasizedLabelSmall = emphasizedStyles.labelSmall?.toString(),
)
}
}
@Serializable
data class TextStyles<out W : FontWeight?>(
val bodySmall: TextStyle<W>? = null,
val bodyMedium: TextStyle<W>? = null,
val bodyLarge: TextStyle<W>? = null,
val labelSmall: TextStyle<W>? = null,
val labelMedium: TextStyle<W>? = null,
val labelLarge: TextStyle<W>? = null,
val titleSmall: TextStyle<W>? = null,
val titleMedium: TextStyle<W>? = null,
val titleLarge: TextStyle<W>? = null,
val headlineSmall: TextStyle<W>? = null,
val headlineMedium: TextStyle<W>? = null,
val headlineLarge: TextStyle<W>? = null,
val displaySmall: TextStyle<W>? = null,
val displayMedium: TextStyle<W>? = null,
val displayLarge: TextStyle<W>? = null,
)
@Serializable
data class TextStyle<out W : FontWeight?>(
/**
* Index of the font family to use for this text style.
*/
val fontFamily: String? = null,
/**
* The font size in sp.
*/
val fontSize: Int? = null,
/**
* The font weight, e.g. 400 for normal, 700 for bold.
*/
@Contextual val fontWeight: W? = null,
/**
* The line height in em.
*/
val lineHeight: Float? = null,
/**
* The letter spacing in em.
*/
val letterSpacing: Float? = null,
) {
/**
* Secondary constructor that uses absolute units for line height and letter spacing.
*/
constructor(
fontFamily: String?,
fontSize: Int,
fontWeight: W?,
lineHeight: Int?,
letterSpacing: Float?,
) : this(
fontFamily = fontFamily,
fontSize = fontSize,
fontWeight = fontWeight,
lineHeight = lineHeight?.toFloat()?.div(fontSize),
letterSpacing = letterSpacing?.div(fontSize)
)
override fun toString(): String {
return ThemeJson.encodeToString<TextStyle<FontWeight?>>(this)
}
companion object {
fun <T : FontWeight> fromString(string: String?): TextStyle<T>? {
if (string.isNullOrEmpty()) return null
return ThemeJson.decodeFromString<TextStyle<FontWeight>>(string) as TextStyle<T>
}
}
}
@Serializable
sealed interface FontFamily {
@Serializable
@SerialName("launcher_default")
data object LauncherDefault : FontFamily
@Serializable
@SerialName("device_headline")
data object DeviceHeadline : FontFamily
@Serializable
@SerialName("device_body")
data object DeviceBody : FontFamily
@Serializable
@SerialName("sans-serif")
data object SansSerif : FontFamily
@Serializable
@SerialName("serif")
data object Serif : FontFamily
@Serializable
@SerialName("monospace")
data object Monospace : FontFamily
@Serializable
@SerialName("system")
data class System(val name: String) : FontFamily
}
@Serializable(with = FontWeightSerializer::class)
sealed interface FontWeight {
/**
* Absolute font weight, e.g. 400 for normal, 700 for bold.
*/
@JvmInline
value class Absolute(val weight: Int) : FontWeight
/**
* Relative font weight, in relation to another font style.
* This is used for emphasized styles, i.e. if the base style has a weight of 400,
* the emphasized style with a relative weight of 100 will have a weight of 500,
*/
@JvmInline
value class Relative(val relativeWeight: Int) : FontWeight
}
@@ -0,0 +1,125 @@
package de.mm20.launcher2.themes.typography
import android.content.Context
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.MonospaceId
import de.mm20.launcher2.themes.R
import de.mm20.launcher2.themes.SerifId
import de.mm20.launcher2.themes.SystemFontId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.util.UUID
class TypographyRepository(
private val context: Context,
private val database: AppDatabase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
fun getAll(): Flow<List<Typography>> {
return database.themeDao().getAllTypographies().map {
getBuiltIn() + it.map { Typography(it) }
}
}
fun get(id: UUID): Flow<Typography?> {
if (id == DefaultThemeId) return flowOf(default)
if (id == SystemFontId) return flowOf(systemFont)
if (id == SerifId) return flowOf(serif)
if (id == MonospaceId) return flowOf(monospace)
return database.themeDao().getTypography(id).map { it?.let { Typography(it) } }
}
fun create(typography: Typography) {
scope.launch {
database.themeDao().insertTypography(typography.toEntity())
}
}
fun update(typography: Typography) {
scope.launch {
database.themeDao().updateTypography(typography.toEntity())
}
}
fun delete(typography: Typography) {
scope.launch {
database.themeDao().deleteTypography(typography.id)
}
}
fun getOrDefault(id: UUID?): Flow<Typography> {
if (id == null) return flowOf(default)
return get(id).map { it ?: default }
}
private fun getBuiltIn(): List<Typography> {
return listOf(
default,
systemFont,
serif,
monospace,
)
}
private val default: Typography
get() = Typography(
id = DefaultThemeId,
builtIn = true,
name = "Outfit",
fonts = mapOf(
"brand" to FontFamily.LauncherDefault,
"plain" to null,
),
styles = DefaultTextStyles,
emphasizedStyles = DefaultEmphasizedTextStyles,
)
private val systemFont: Typography
get() = Typography(
id = SystemFontId,
builtIn = true,
name = context.getString(R.string.preference_font_system),
fonts = mapOf(
"brand" to FontFamily.DeviceHeadline,
"plain" to FontFamily.DeviceBody,
),
styles = DefaultTextStyles,
emphasizedStyles = DefaultEmphasizedTextStyles,
)
private val serif: Typography
get() = Typography(
id = SerifId,
builtIn = true,
name = "Serif",
fonts = mapOf(
"brand" to FontFamily.Serif,
"plain" to FontFamily.Serif,
),
styles = DefaultTextStyles,
emphasizedStyles = DefaultEmphasizedTextStyles,
)
private val monospace: Typography
get() = Typography(
id = MonospaceId,
builtIn = true,
name = "Monospace",
fonts = mapOf(
"brand" to FontFamily.Monospace,
"plain" to FontFamily.Monospace,
),
styles = DefaultTextStyles,
emphasizedStyles = DefaultEmphasizedTextStyles,
)
}