(feat) custom typography schemes

This commit is contained in:
MM20 2025-06-18 22:28:13 +02:00
parent 4bf9ee8b0c
commit da8416a58c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
31 changed files with 2856 additions and 117 deletions

View File

@ -84,6 +84,10 @@ import de.mm20.launcher2.ui.settings.transparencies.TransparencySchemeSettingsRo
import de.mm20.launcher2.ui.settings.transparencies.TransparencySchemeSettingsScreen
import de.mm20.launcher2.ui.settings.transparencies.TransparencySchemesSettingsRoute
import de.mm20.launcher2.ui.settings.transparencies.TransparencySchemesSettingsScreen
import de.mm20.launcher2.ui.settings.typography.TypographiesSettingsRoute
import de.mm20.launcher2.ui.settings.typography.TypographiesSettingsScreen
import de.mm20.launcher2.ui.settings.typography.TypographySettingsRoute
import de.mm20.launcher2.ui.settings.typography.TypographySettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterHelpSettingsScreen
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
import de.mm20.launcher2.ui.settings.weather.WeatherIntegrationSettingsScreen
@ -218,6 +222,14 @@ class SettingsActivity : BaseActivity() {
?: return@composable
TransparencySchemeSettingsScreen(UUID.fromString(route.id))
}
composable<TypographiesSettingsRoute> {
TypographiesSettingsScreen()
}
composable<TypographySettingsRoute> {
val route: TypographySettingsRoute = it.toRoute()
?: return@composable
TypographySettingsScreen(UUID.fromString(route.id))
}
composable("settings/appearance/cards") {
CardsSettingsScreen()
}

View File

@ -29,14 +29,15 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.value
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.settings.transparencies.TransparencySchemesSettingsRoute
import de.mm20.launcher2.ui.settings.typography.TypographiesSettingsRoute
import de.mm20.launcher2.ui.theme.getTypography
@Composable
fun AppearanceSettingsScreen() {
val viewModel: AppearanceSettingsScreenVM = viewModel()
val context = LocalContext.current
val navController = LocalNavController.current
val colorThemeName by viewModel.colorThemeName.collectAsStateWithLifecycle(null)
val typographyThemeName by viewModel.typographyThemeName.collectAsStateWithLifecycle(null)
val shapeThemeName by viewModel.shapeThemeName.collectAsStateWithLifecycle(null)
val transparencyThemeName by viewModel.transparencyThemeName.collectAsStateWithLifecycle(null)
val compatModeColors by viewModel.compatModeColors.collectAsState()
@ -77,22 +78,11 @@ fun AppearanceSettingsScreen() {
},
icon = Icons.Rounded.Palette,
)
val font by viewModel.font.collectAsState()
ListPreference(
title = stringResource(R.string.preference_font),
items = listOf(
"Outfit" to Font.Outfit,
stringResource(R.string.preference_font_system) to Font.System,
),
value = font,
onValueChanged = {
if (it != null) viewModel.setFont(it)
},
itemLabel = {
val typography = remember(it.value) {
getTypography(context, it.value)
}
Text(it.first, style = typography.titleMedium)
Preference(
title = stringResource(id = R.string.preference_screen_typography),
summary = typographyThemeName,
onClick = {
navController?.navigate(TypographiesSettingsRoute)
},
icon = Icons.Rounded.TextFields,
)

View File

@ -39,6 +39,13 @@ class AppearanceSettingsScreenVM : ViewModel(), KoinComponent {
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val typographyThemeName = uiSettings.typographyId.flatMapLatest {
themeRepository.typographies.getOrDefault(it)
}.map {
it.name
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val transparencyThemeName = uiSettings.transparenciesId.flatMapLatest {
themeRepository.transparencies.getOrDefault(it)
}.map {

View File

@ -65,6 +65,7 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.settings.transparencies.checkerboard
import de.mm20.launcher2.ui.settings.typography.PreviewTexts
import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf
import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf
import de.mm20.launcher2.ui.theme.shapes.shapesOf
@ -233,6 +234,7 @@ private fun ThemePreview(
darkMode: Boolean,
onDarkModeChanged: (Boolean) -> Unit,
) {
val previewTexts = PreviewTexts()
Column(
modifier = Modifier
.fillMaxWidth()
@ -291,17 +293,17 @@ private fun ThemePreview(
modifier = Modifier.fillMaxWidth(),
) {
Text(
"Title",
previewTexts.Medium1,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
"Subtitle",
previewTexts.Medium2,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.secondary
)
Text(
"Body",
previewTexts.TwoLines,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 4.dp),
color = MaterialTheme.colorScheme.onSurface,
@ -316,7 +318,7 @@ private fun ThemePreview(
) {
FilterChip(
selected = true,
label = { Text("Chip") },
label = { Text(previewTexts.Short1) },
leadingIcon = {
Icon(
Icons.Rounded.Star, null,
@ -327,7 +329,7 @@ private fun ThemePreview(
)
FilterChip(
selected = false,
label = { Text("Chip") },
label = { Text(previewTexts.Short2) },
leadingIcon = {
Icon(
Icons.Rounded.Star, null,
@ -348,8 +350,8 @@ private fun ThemePreview(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
Button(onClick = {}) { Text("Button") }
OutlinedButton(onClick = {}) { Text("Button") }
Button(onClick = {}) { Text(previewTexts.Medium1) }
OutlinedButton(onClick = {}) { Text(previewTexts.Medium2) }
}
}
Box(

View File

@ -18,7 +18,6 @@ import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RadioButtonChecked
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@ -61,6 +60,8 @@ fun ColorSchemesSettingsScreen() {
var deleteColors by remember { mutableStateOf<Colors?>(null) }
val (builtin, user) = themes.partition { it.builtIn }
PreferenceScreen(
title = stringResource(R.string.preference_screen_colors),
topBarActions = {
@ -71,7 +72,7 @@ fun ColorSchemesSettingsScreen() {
) {
item {
PreferenceCategory {
for (theme in themes) {
for (theme in builtin) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
@ -90,18 +91,6 @@ fun ColorSchemesSettingsScreen() {
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (!theme.builtIn) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Edit, null)
},
text = { Text(stringResource(R.string.edit)) },
onClick = {
navController?.navigate("settings/appearance/colors/${theme.id}")
showMenu = false
}
)
}
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
@ -112,14 +101,56 @@ fun ColorSchemesSettingsScreen() {
showMenu = false
}
)
if (!theme.builtIn) {
}
}
},
onClick = {
viewModel.selectTheme(theme)
}
)
}
}
}
if (user.isNotEmpty()) {
item {
PreferenceCategory {
for (theme in user) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
title = theme.name,
controls = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
ColorSchemePreview(theme)
IconButton(
modifier = Modifier.padding(start = 12.dp),
onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Share, null)
Icon(Icons.Rounded.Edit, null)
},
text = { Text(stringResource(R.string.menu_share)) },
text = { Text(stringResource(R.string.edit)) },
onClick = {
viewModel.exportTheme(context, theme)
navController?.navigate("settings/appearance/colors/${theme.id}")
showMenu = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
@ -135,12 +166,12 @@ fun ColorSchemesSettingsScreen() {
)
}
}
},
onClick = {
viewModel.selectTheme(theme)
}
},
onClick = {
viewModel.selectTheme(theme)
}
)
)
}
}
}
}

View File

@ -54,6 +54,8 @@ fun ShapeSchemesSettingsScreen() {
var deleteShapes by remember { mutableStateOf<Shapes?>(null) }
val (builtin, user) = themes.partition { it.builtIn }
PreferenceScreen(
title = stringResource(R.string.preference_screen_shapes),
topBarActions = {
@ -64,7 +66,7 @@ fun ShapeSchemesSettingsScreen() {
) {
item {
PreferenceCategory {
for (theme in themes) {
for (theme in builtin) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
@ -83,18 +85,6 @@ fun ShapeSchemesSettingsScreen() {
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (!theme.builtIn) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Edit, null)
},
text = { Text(stringResource(R.string.edit)) },
onClick = {
navController?.navigate("settings/appearance/shapes/${theme.id}")
showMenu = false
}
)
}
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
@ -105,7 +95,58 @@ fun ShapeSchemesSettingsScreen() {
showMenu = false
}
)
if (!theme.builtIn) {
}
}
},
onClick = {
viewModel.selectShapes(theme)
}
)
}
}
}
if (user.isNotEmpty()) {
item {
PreferenceCategory {
for (theme in user) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
title = theme.name,
controls = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
ShapesPreview(theme)
IconButton(
modifier = Modifier.padding(start = 12.dp),
onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Edit, null)
},
text = { Text(stringResource(R.string.edit)) },
onClick = {
navController?.navigate("settings/appearance/shapes/${theme.id}")
showMenu = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Delete, null)
@ -118,12 +159,12 @@ fun ShapeSchemesSettingsScreen() {
)
}
}
},
onClick = {
viewModel.selectShapes(theme)
}
},
onClick = {
viewModel.selectShapes(theme)
}
)
)
}
}
}
}

View File

@ -20,7 +20,6 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation
@ -33,8 +32,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -65,6 +62,8 @@ fun TransparencySchemesSettingsScreen() {
var deleteTransparencies by remember { mutableStateOf<Transparencies?>(null) }
val (builtin, user) = themes.partition { it.builtIn }
val wallpaperColors = wallpaperColorsAsState().value
PreferenceScreen(
@ -77,7 +76,7 @@ fun TransparencySchemesSettingsScreen() {
) {
item {
PreferenceCategory {
for (theme in themes) {
for (theme in builtin) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
@ -96,7 +95,48 @@ fun TransparencySchemesSettingsScreen() {
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
if (!theme.builtIn) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
}
}
},
onClick = {
viewModel.selectTransparencies(theme)
}
)
}
}
}
if (user.isNotEmpty()) {
item {
PreferenceCategory {
for (theme in user) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
title = theme.name,
controls = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
TransparenciesPreview(wallpaperColors, theme)
IconButton(
modifier = Modifier.padding(start = 12.dp),
onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Edit, null)
@ -109,18 +149,16 @@ fun TransparencySchemesSettingsScreen() {
showMenu = false
}
)
}
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
if (!theme.builtIn) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Delete, null)
@ -133,12 +171,12 @@ fun TransparencySchemesSettingsScreen() {
)
}
}
},
onClick = {
viewModel.selectTransparencies(theme)
}
},
onClick = {
viewModel.selectTransparencies(theme)
}
)
)
}
}
}
}
@ -193,13 +231,19 @@ private fun TransparenciesPreview(wallpaperColors: WallpaperColors, theme: Trans
) {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceContainer.copy(alpha = transparencies.background), MaterialTheme.shapes.extraSmall)
.background(
MaterialTheme.colorScheme.surfaceContainer.copy(alpha = transparencies.background),
MaterialTheme.shapes.extraSmall
)
.height(40.dp)
.width(56.dp)
)
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface.copy(alpha = transparencies.surface), MaterialTheme.shapes.extraSmall)
.background(
MaterialTheme.colorScheme.surface.copy(alpha = transparencies.surface),
MaterialTheme.shapes.extraSmall
)
.height(24.dp)
.width(48.dp)
)

View File

@ -0,0 +1,88 @@
package de.mm20.launcher2.ui.settings.typography
import android.icu.util.ULocale
/**
* Preview texts to demonstrate typography settings in the users dominant script.
* Preview texts should be similar in length, and they should be language-neutral, and not use
* letters that are used only in some languages.
*/
interface PreviewTexts {
/**
* Preview for the font. Should be a character that is representative for a font.
*/
val ExtraShort: String
/**
* A short, 3-letter preview.
*/
val Short1: String
/**
* An alternative to [Short1], also 3 letters.
*/
val Short2: String
/**
* A medium-length preview, 5-6 letters.
*/
val Medium1: String
/**
* An alternative to [Medium1], also 5-6 letters.
*/
val Medium2: String
/**
* A longer preview, to demonstrate line height.
* Should contain letters and numbers, and a newline.
*/
val TwoLines: String
companion object {
operator fun invoke(): PreviewTexts {
val script = ULocale.addLikelySubtags(ULocale.getDefault()).script
return forScript(script)
}
/**
* Returns the appropriate [PreviewTexts] implementation based on the script.
* Defaults to [LatinPreviewTexts] if the script is not recognized.
* @param script the ISO-15924 script code
*/
fun forScript(script: String): PreviewTexts {
return when (script) {
"Latn" -> LatinPreviewTexts
"Cyrl" -> CyrillicPreviewTexts
"Grek" -> GreekPreviewTexts
else -> LatinPreviewTexts
}
}
}
}
object LatinPreviewTexts: PreviewTexts {
override val ExtraShort: String = "Aa"
override val Short1: String = "Abc"
override val Short2: String = "Deg"
override val Medium1: String = "Abcdeg"
override val Medium2: String = "Hilmno"
override val TwoLines: String = "Abcdeghilm Nop\nRst 123456890"
}
object CyrillicPreviewTexts: PreviewTexts {
override val ExtraShort: String = "Аа"
override val Short1: String = "Абв"
override val Short2: String = "Где"
override val Medium1: String = "Абвгде"
override val Medium2: String = "Жзиклм"
override val TwoLines: String = "Абвгдежзик Лмн\nОпр 1234567890"
}
object GreekPreviewTexts: PreviewTexts {
override val ExtraShort: String = "Αα"
override val Short1: String = "Αβγ"
override val Short2: String = "Δεζ"
override val Medium1: String = "Αβγδεζ"
override val Medium2: String = "Ηθικλμ"
override val TwoLines: String = "Αβγδεζηθ Ικλμ\nΝξο 1234567890"
}

View File

@ -0,0 +1,253 @@
package de.mm20.launcher2.ui.settings.typography
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RadioButtonChecked
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.themes.typography.Typography
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.theme.typography.typographyOf
import kotlinx.serialization.Serializable
@Serializable
data object TypographiesSettingsRoute
@Composable
fun TypographiesSettingsScreen() {
val viewModel: TypographySettingsScreenVM = viewModel()
val navController = LocalNavController.current
val context = LocalContext.current
val selectedTheme by viewModel.selectedTypography.collectAsStateWithLifecycle(null)
val themes by viewModel.typography.collectAsStateWithLifecycle(emptyList())
var deleteTypography by remember { mutableStateOf<Typography?>(null) }
val (builtin, user) = themes.partition { it.builtIn }
PreferenceScreen(
title = stringResource(R.string.preference_screen_typography),
topBarActions = {
IconButton(onClick = { viewModel.createNew(context) }) {
Icon(Icons.Rounded.Add, null)
}
},
) {
item {
PreferenceCategory {
for (theme in builtin) {
var showMenu by remember { mutableStateOf(false) }
val typo = typographyOf(theme)
Preference(
icon = {
Icon(
if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
text = theme.name,
maxLines = 1,
style = typo.titleMedium
)
},
controls = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
TypographyPreview(typo)
IconButton(
modifier = Modifier.padding(start = 12.dp),
onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
}
}
},
onClick = {
viewModel.selectTypography(theme)
}
)
}
}
}
if (user.isNotEmpty()) {
item {
PreferenceCategory {
for (theme in user) {
var showMenu by remember { mutableStateOf(false) }
val typo = typographyOf(theme)
Preference(
icon = {
Icon(
if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
},
title = {
Text(
text = theme.name,
maxLines = 1,
style = typo.titleMedium
)
},
controls = {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
TypographyPreview(typo)
IconButton(
modifier = Modifier.padding(start = 12.dp),
onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Edit, null)
},
text = { Text(stringResource(R.string.edit)) },
onClick = {
navController?.navigate(
TypographySettingsRoute(theme.id.toString())
)
showMenu = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.ContentCopy, null)
},
text = { Text(stringResource(R.string.duplicate)) },
onClick = {
viewModel.duplicate(theme)
showMenu = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Delete, null)
},
text = { Text(stringResource(R.string.menu_delete)) },
onClick = {
deleteTypography = theme
showMenu = false
}
)
}
}
},
onClick = {
viewModel.selectTypography(theme)
}
)
}
}
}
}
}
if (deleteTypography != null) {
AlertDialog(
onDismissRequest = { deleteTypography = null },
text = {
Text(
stringResource(
R.string.confirmation_delete_transparencies_scheme,
deleteTypography!!.name
)
)
},
confirmButton = {
TextButton(
onClick = {
viewModel.delete(deleteTypography!!)
deleteTypography = null
}
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(
onClick = { deleteTypography = null }
) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
}
@Composable
private fun TypographyPreview(typography: androidx.compose.material3.Typography) {
val previewTexts = PreviewTexts()
Column(
modifier = Modifier.padding(vertical = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = previewTexts.Short1,
style = typography.titleSmall,
)
Text(
text = previewTexts.Short2,
style = typography.bodySmall,
color = MaterialTheme.colorScheme.secondary,
)
}
}

View File

@ -0,0 +1,50 @@
package de.mm20.launcher2.ui.settings.typography
import android.content.Context
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.themes.ThemeRepository
import de.mm20.launcher2.themes.typography.Typography
import de.mm20.launcher2.ui.R
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.UUID
class TypographySettingsScreenVM : ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
private val uiSettings: UiSettings by inject()
val selectedTypography = uiSettings.typographyId
val typography: Flow<List<Typography>> = themeRepository.typographies.getAll()
fun getTypography(id: UUID): Flow<Typography?> {
return themeRepository.typographies.get(id)
}
fun updateTypography(typography: Typography) {
themeRepository.typographies.update(typography)
}
fun selectTypography(typography: Typography) {
uiSettings.setTypographyId(typography.id)
}
fun duplicate(typography: Typography) {
themeRepository.typographies.create(typography.copy(id = UUID.randomUUID()))
}
fun delete(typography: Typography) {
themeRepository.typographies.delete(typography)
}
fun createNew(context: Context) {
themeRepository.typographies.create(
Typography(
id = UUID.randomUUID(),
name = context.getString(R.string.new_theme_name)
)
)
}
}

View File

@ -15,6 +15,7 @@ import de.mm20.launcher2.ui.theme.transparency.LocalTransparencyScheme
import de.mm20.launcher2.ui.theme.transparency.transparencySchemeOf
import de.mm20.launcher2.ui.theme.typography.DefaultTypography
import de.mm20.launcher2.ui.theme.typography.getDeviceDefaultTypography
import de.mm20.launcher2.ui.theme.typography.typographyOf
import kotlinx.coroutines.flow.flatMapLatest
import org.koin.compose.koinInject
import de.mm20.launcher2.preferences.ColorScheme as ColorSchemePref
@ -24,8 +25,6 @@ import de.mm20.launcher2.preferences.ColorScheme as ColorSchemePref
fun LauncherTheme(
content: @Composable () -> Unit
) {
val context = LocalContext.current
val uiSettings: UiSettings = koinInject()
val themeRepository: ThemeRepository = koinInject()
@ -41,6 +40,12 @@ fun LauncherTheme(
}
}.collectAsState(null)
val themeTypography by remember {
uiSettings.typographyId.flatMapLatest {
themeRepository.typographies.getOrDefault(it)
}
}.collectAsState(null)
val themeTransparencies by remember {
uiSettings.transparenciesId.flatMapLatest {
themeRepository.transparencies.getOrDefault(it)
@ -53,7 +58,7 @@ fun LauncherTheme(
val darkTheme =
colorSchemePref == ColorSchemePref.Dark || colorSchemePref == ColorSchemePref.System && isSystemInDarkTheme()
if (themeColors == null || themeShapes == null || themeTransparencies == null) {
if (themeColors == null || themeShapes == null || themeTransparencies == null || themeTypography == null) {
return
}
@ -64,6 +69,7 @@ fun LauncherTheme(
}
val shapes = shapesOf(themeShapes!!)
val typography = typographyOf(themeTypography!!)
val transparencyScheme = transparencySchemeOf(themeTransparencies!!)
@ -72,10 +78,6 @@ fun LauncherTheme(
Font.Outfit
)
val typography = remember(font) {
getTypography(context, font)
}
CompositionLocalProvider(
LocalDarkTheme provides darkTheme,
LocalTransparencyScheme provides transparencyScheme,

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.theme.colorscheme
import android.R
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
@ -89,11 +88,11 @@ fun systemCorePalette(): CorePalette<Int> {
if (Build.VERSION.SDK_INT >= 31 && !compatModeColors) {
val context = LocalContext.current
return CorePalette(
primary = ContextCompat.getColor(context, R.color.system_accent1_500),
secondary = ContextCompat.getColor(context, R.color.system_accent2_500),
tertiary = ContextCompat.getColor(context, R.color.system_accent3_500),
neutral = ContextCompat.getColor(context, R.color.system_neutral1_500),
neutralVariant = ContextCompat.getColor(context, R.color.system_neutral2_500),
primary = ContextCompat.getColor(context, android.R.color.system_accent1_500),
secondary = ContextCompat.getColor(context, android.R.color.system_accent2_500),
tertiary = ContextCompat.getColor(context, android.R.color.system_accent3_500),
neutral = ContextCompat.getColor(context, android.R.color.system_neutral1_500),
neutralVariant = ContextCompat.getColor(context, android.R.color.system_neutral2_500),
error = 0xFFB3261E.toInt(),
)
}

View File

@ -23,7 +23,7 @@ fun makeTypography(
headlineSmallEmphasized = baseTypography.headlineSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold),
titleLarge = baseTypography.titleLarge.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold),
titleLargeEmphasized = baseTypography.titleLargeEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold),
titleMedium = baseTypography.titleMedium.copy(fontFamily = headlineFamily),
titleMedium = baseTypography.titleMedium.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold),
titleMediumEmphasized = baseTypography.titleMediumEmphasized.copy(fontFamily = headlineFamily),
titleSmall = baseTypography.titleSmall.copy(fontFamily = headlineFamily, fontWeight = FontWeight.SemiBold),
titleSmallEmphasized = baseTypography.titleSmallEmphasized.copy(fontFamily = headlineFamily, fontWeight = FontWeight.Bold),

View File

@ -0,0 +1,327 @@
package de.mm20.launcher2.ui.theme.typography
import android.content.Context
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import de.mm20.launcher2.themes.typography.DefaultEmphasizedTextStyles
import de.mm20.launcher2.themes.typography.DefaultTextStyles
import de.mm20.launcher2.ui.theme.typography.fontfamily.Outfit
import de.mm20.launcher2.ui.theme.typography.fontfamily.getDeviceBodyFontFamily
import de.mm20.launcher2.ui.theme.typography.fontfamily.getDeviceHeadlineFontFamily
import de.mm20.launcher2.themes.typography.FontFamily as ThemeFontFamily
import de.mm20.launcher2.themes.typography.FontWeight as ThemeFontWeight
import de.mm20.launcher2.themes.typography.TextStyle as ThemeTextStyle
import de.mm20.launcher2.themes.typography.Typography as ThemeTypography
@Composable
fun typographyOf(typography: ThemeTypography): Typography {
val context = LocalContext.current
return remember(context, typography) {
val base = Typography()
val fonts = getFontFamilies(context, typography.fonts)
base.copy(
displayLarge = textStyleOf(
typography.styles.displayLarge,
DefaultTextStyles.displayLarge!!,
base.displayLarge,
fonts,
),
displayLargeEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.displayLarge,
typography.styles.displayLarge,
DefaultEmphasizedTextStyles.displayLarge!!,
DefaultTextStyles.displayLarge!!,
base.displayLargeEmphasized,
fonts,
),
displayMedium = textStyleOf(
typography.styles.displayMedium,
DefaultTextStyles.displayMedium!!,
base.displayMedium,
fonts,
),
displayMediumEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.displayMedium,
typography.styles.displayMedium,
DefaultEmphasizedTextStyles.displayMedium!!,
DefaultTextStyles.displayMedium!!,
base.displayMediumEmphasized,
fonts,
),
displaySmall = textStyleOf(
typography.styles.displaySmall,
DefaultTextStyles.displaySmall!!,
base.displaySmall,
fonts,
),
displaySmallEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.displaySmall,
typography.styles.displaySmall,
DefaultEmphasizedTextStyles.displaySmall!!,
DefaultTextStyles.displaySmall!!,
base.displaySmallEmphasized,
fonts,
),
headlineLarge = textStyleOf(
typography.styles.headlineLarge,
DefaultTextStyles.headlineLarge!!,
base.headlineLarge,
fonts,
),
headlineLargeEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.headlineLarge,
typography.styles.headlineLarge,
DefaultEmphasizedTextStyles.headlineLarge!!,
DefaultTextStyles.headlineLarge!!,
base.headlineLargeEmphasized,
fonts,
),
headlineMedium = textStyleOf(
typography.styles.headlineMedium,
DefaultTextStyles.headlineMedium!!,
base.headlineMedium,
fonts,
),
headlineMediumEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.headlineMedium,
typography.styles.headlineMedium,
DefaultEmphasizedTextStyles.headlineMedium!!,
DefaultTextStyles.headlineMedium!!,
base.headlineMediumEmphasized,
fonts,
),
headlineSmall = textStyleOf(
typography.styles.headlineSmall,
DefaultTextStyles.headlineSmall!!,
base.headlineSmall,
fonts,
),
headlineSmallEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.headlineSmall,
typography.styles.headlineSmall,
DefaultEmphasizedTextStyles.headlineSmall!!,
DefaultTextStyles.headlineSmall!!,
base.headlineSmallEmphasized,
fonts,
),
titleLarge = textStyleOf(
typography.styles.titleLarge,
DefaultTextStyles.titleLarge!!,
base.titleLarge,
fonts,
),
titleLargeEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.titleLarge,
typography.styles.titleLarge,
DefaultEmphasizedTextStyles.titleLarge!!,
DefaultTextStyles.titleLarge!!,
base.titleLargeEmphasized,
fonts,
),
titleMedium = textStyleOf(
typography.styles.titleMedium,
DefaultTextStyles.titleMedium!!,
base.titleMedium,
fonts,
),
titleMediumEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.titleMedium,
typography.styles.titleMedium,
DefaultEmphasizedTextStyles.titleMedium!!,
DefaultTextStyles.titleMedium!!,
base.titleMediumEmphasized,
fonts,
),
titleSmall = textStyleOf(
typography.styles.titleSmall,
DefaultTextStyles.titleSmall!!,
base.titleSmall,
fonts,
),
titleSmallEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.titleSmall,
typography.styles.titleSmall,
DefaultEmphasizedTextStyles.titleSmall!!,
DefaultTextStyles.titleSmall!!,
base.titleSmallEmphasized,
fonts,
),
bodyLarge = textStyleOf(
typography.styles.bodyLarge,
DefaultTextStyles.bodyLarge!!,
base.bodyLarge,
fonts,
),
bodyLargeEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.bodyLarge,
typography.styles.bodyLarge,
DefaultEmphasizedTextStyles.bodyLarge!!,
DefaultTextStyles.bodyLarge!!,
base.bodyLargeEmphasized,
fonts,
),
bodyMedium = textStyleOf(
typography.styles.bodyMedium,
DefaultTextStyles.bodyMedium!!,
base.bodyMedium,
fonts,
),
bodyMediumEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.bodyMedium,
typography.styles.bodyMedium,
DefaultEmphasizedTextStyles.bodyMedium!!,
DefaultTextStyles.bodyMedium!!,
base.bodyMediumEmphasized,
fonts,
),
bodySmall = textStyleOf(
typography.styles.bodySmall,
DefaultTextStyles.bodySmall!!,
base.bodySmall,
fonts,
),
bodySmallEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.bodySmall,
typography.styles.bodySmall,
DefaultEmphasizedTextStyles.bodySmall!!,
DefaultTextStyles.bodySmall!!,
base.bodySmallEmphasized,
fonts,
),
labelLarge = textStyleOf(
typography.styles.labelLarge,
DefaultTextStyles.labelLarge!!,
base.labelLarge,
fonts,
),
labelLargeEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.labelLarge,
typography.styles.labelLarge,
DefaultEmphasizedTextStyles.labelLarge!!,
DefaultTextStyles.labelLarge!!,
base.labelLargeEmphasized,
fonts,
),
labelMedium = textStyleOf(
typography.styles.labelMedium,
DefaultTextStyles.labelMedium!!,
base.labelMedium,
fonts,
),
labelMediumEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.labelMedium,
typography.styles.labelMedium,
DefaultEmphasizedTextStyles.labelMedium!!,
DefaultTextStyles.labelMedium!!,
base.labelMediumEmphasized,
fonts,
),
labelSmall = textStyleOf(
typography.styles.labelSmall,
DefaultTextStyles.labelSmall!!,
base.labelSmall,
fonts,
),
labelSmallEmphasized = emphasizedTextStyleOf(
typography.emphasizedStyles.labelSmall,
typography.styles.labelSmall,
DefaultEmphasizedTextStyles.labelSmall!!,
DefaultTextStyles.labelSmall!!,
base.labelSmallEmphasized,
fonts,
),
)
}
}
private fun textStyleOf(
style: ThemeTextStyle<ThemeFontWeight.Absolute?>?,
fallback: ThemeTextStyle<ThemeFontWeight.Absolute?>,
base: TextStyle,
fonts: Map<String, FontFamily?>,
): TextStyle {
return base.copy(
fontFamily = (style?.fontFamily ?: fallback.fontFamily)?.let { fonts[it] }
?: base.fontFamily,
fontWeight = (style?.fontWeight?.weight
?: fallback.fontWeight?.weight)?.let { FontWeight(it) } ?: base.fontWeight,
fontSize = (style?.fontSize ?: fallback.fontSize)?.sp ?: base.fontSize,
lineHeight = (style?.lineHeight ?: fallback.lineHeight)?.em ?: base.lineHeight,
letterSpacing = (style?.letterSpacing ?: fallback.letterSpacing)?.em ?: base.letterSpacing,
)
}
private fun emphasizedTextStyleOf(
style: ThemeTextStyle<ThemeFontWeight?>?,
parent: ThemeTextStyle<ThemeFontWeight.Absolute?>?,
fallback: ThemeTextStyle<ThemeFontWeight?>,
fallbackParent: ThemeTextStyle<ThemeFontWeight.Absolute?>,
base: TextStyle,
fonts: Map<String, FontFamily?>,
): TextStyle {
val weight: ThemeFontWeight? = style?.fontWeight ?: fallback.fontWeight
val parentWeight = parent?.fontWeight ?: fallbackParent.fontWeight
val fontWeight = when (weight) {
is ThemeFontWeight.Absolute -> FontWeight(weight.weight)
is ThemeFontWeight.Relative if (parentWeight != null) -> {
FontWeight(parentWeight.weight + weight.relativeWeight)
}
else -> base.fontWeight
}
return base.copy(
fontFamily = (style?.fontFamily
?: parent?.fontFamily
?: fallback.fontFamily
?: fallbackParent.fontFamily)
?.let { fonts[it] }
?: base.fontFamily,
fontWeight = fontWeight,
fontSize = (style?.fontSize ?: parent?.fontSize ?: fallback.fontSize
?: fallbackParent.fontSize)?.sp ?: base.fontSize,
lineHeight = (style?.lineHeight ?: parent?.lineHeight ?: fallback.lineHeight
?: fallbackParent.lineHeight)?.em
?: base.lineHeight,
letterSpacing = (style?.letterSpacing ?: parent?.letterSpacing
?: fallback.letterSpacing ?: fallbackParent.letterSpacing)?.em ?: base.letterSpacing,
)
}
private fun getFontFamilies(
context: Context,
fonts: Map<String, ThemeFontFamily?>
): Map<String, FontFamily?> {
val distinct = fonts.values.distinct()
val map: Map<ThemeFontFamily?, FontFamily> = distinct.associateWith {
fontFamilyOf(context, it)
}
return fonts.keys.associateWith { map[fonts[it]] }
}
fun fontFamilyOf(
context: Context,
fontFamily: ThemeFontFamily?
): FontFamily {
return when (fontFamily) {
is ThemeFontFamily.LauncherDefault -> Outfit
is ThemeFontFamily.DeviceHeadline -> getDeviceHeadlineFontFamily(context)
is ThemeFontFamily.DeviceBody -> getDeviceBodyFontFamily(context)
is ThemeFontFamily.SansSerif -> FontFamily.SansSerif
is ThemeFontFamily.Serif -> FontFamily.Serif
is ThemeFontFamily.Monospace -> FontFamily.Monospace
else -> FontFamily.Default
}
}

View File

@ -41,4 +41,41 @@ fun getDeviceHeadlineFontFamily(context: Context): FontFamily {
return FontFamily.SansSerif
}
fun getDeviceBodyFontFamily(context: Context): FontFamily {
val configResId = context.resources
.getIdentifier("config_bodyFontFamily", "string", "android")
if (configResId != 0) {
val fontFamily = context.resources.getString(configResId)
if (fontFamily.isBlank()) return FontFamily.SansSerif
return try {
FontFamily(
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Thin, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.ExtraLight, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Light, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Normal, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Medium, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.SemiBold, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Bold, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.ExtraBold, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Black, style = FontStyle.Normal),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Thin, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.ExtraLight, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Light, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Normal, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Medium, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.SemiBold, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Bold, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.ExtraBold, style = FontStyle.Italic),
Font(DeviceFontFamilyName(fontFamily), weight = FontWeight.Black, style = FontStyle.Italic),
)
} catch (e: IllegalArgumentException) {
FontFamily.SansSerif
}
}
return FontFamily.SansSerif
}

View File

@ -1834,4 +1834,69 @@ val _RoundedCornerAlt = materialIcon("Icons.Rounded.RoundedCornerAlt") {
}
val Icons.Rounded.RoundedCornerAlt
get() = _RoundedCornerAlt
get() = _RoundedCornerAlt
private val _LetterSpacing2 = materialIcon("Icons.Rounded.LetterSpacing2") {
materialPath {
moveTo(6.7f, 21.5125f)
quadToRelative(-0.275f, 0.275f, -0.7f, 0.275f)
quadToRelative(-0.425f, 0f, -0.7f, -0.275f)
lineToRelative(-2.6f, -2.6f)
quadToRelative(-0.3f, -0.3f, -0.3f, -0.7f)
quadToRelative(0f, -0.4f, 0.3f, -0.7f)
lineToRelative(2.6f, -2.6f)
quadToRelative(0.275f, -0.275f, 0.6875f, -0.275f)
quadToRelative(0.4125f, 0f, 0.7125f, 0.275f)
quadToRelative(0.3f, 0.3f, 0.3f, 0.7125f)
quadToRelative(0f, 0.4125f, -0.3f, 0.7125f)
lineToRelative(-0.875f, 0.875f)
horizontalLineToRelative(12.35f)
lineToRelative(-0.9f, -0.9f)
quadTo(17f, 16.0375f, 17f, 15.625f)
quadToRelative(0f, -0.4125f, 0.3f, -0.7125f)
quadToRelative(0.275f, -0.275f, 0.7f, -0.275f)
quadToRelative(0.425f, 0f, 0.7f, 0.275f)
lineToRelative(2.6f, 2.6f)
quadToRelative(0.3f, 0.3f, 0.3f, 0.7f)
quadToRelative(0f, 0.4f, -0.3f, 0.7f)
lineToRelative(-2.6f, 2.6f)
quadToRelative(-0.275f, 0.275f, -0.6875f, 0.275f)
quadToRelative(-0.4125f, 0f, -0.7125f, -0.275f)
quadToRelative(-0.3f, -0.3f, -0.3f, -0.7125f)
quadToRelative(0f, -0.4125f, 0.3f, -0.7125f)
lineToRelative(0.875f, -0.875f)
horizontalLineTo(5.825f)
lineToRelative(0.9f, 0.9f)
quadTo(7f, 20.3875f, 7f, 20.8f)
quadTo(7f, 21.2125f, 6.7f, 21.5125f)
close()
moveToRelative(0.65f, -9.5f)
lineToRelative(3.425f, -9.2f)
quadTo(10.875f, 2.5375f, 11.1125f, 2.375f)
quadTo(11.35f, 2.2125f, 11.65f, 2.2125f)
horizontalLineToRelative(0.7f)
quadToRelative(0.3f, 0f, 0.5375f, 0.1625f)
quadToRelative(0.2375f, 0.1625f, 0.3375f, 0.4375f)
lineToRelative(3.425f, 9.225f)
quadToRelative(0.15f, 0.425f, -0.1f, 0.8f)
quadToRelative(-0.25f, 0.375f, -0.7f, 0.375f)
quadToRelative(-0.275f, 0f, -0.5125f, -0.1625f)
quadTo(15.1f, 12.8875f, 15f, 12.6125f)
lineToRelative(-0.75f, -2.2f)
horizontalLineTo(9.8f)
lineTo(9f, 12.6375f)
quadToRelative(-0.1f, 0.275f, -0.325f, 0.425f)
quadToRelative(-0.225f, 0.15f, -0.5f, 0.15f)
quadToRelative(-0.475f, 0f, -0.7375f, -0.3875f)
quadTo(7.175f, 12.4375f, 7.35f, 12.0125f)
close()
moveToRelative(3f, -3.2f)
horizontalLineToRelative(3.3f)
lineToRelative(-1.6f, -4.55f)
horizontalLineToRelative(-0.1f)
close()
}
}
val Icons.Rounded.LetterSpacing2
get() = _LetterSpacing2

View File

@ -439,6 +439,7 @@
<string name="preference_shapes_extra_round">Extra round</string>
<string name="preference_shapes_rect">Rectangular</string>
<string name="preference_shapes_base">Base shape</string>
<string name="preference_screen_typography">Typography</string>
<string name="preference_screen_transparencies">Transparency</string>
<string name="preference_transparencies_default">Default</string>
<string name="preference_transparencies_semi_transparent">Semi-transparent</string>
@ -824,6 +825,7 @@
<string name="note_widget_file_write_error_description">The note could not be written to the linked file. Possibly, it has been moved or deleted. A copy has been saved to the launcher\'s internal storage.</string>
<string name="confirmation_delete_color_scheme">Do you really want to delete the color scheme %1$s\?</string>
<string name="confirmation_delete_shapes_scheme">Do you really want to delete the shape scheme %1$s?</string>
<string name="confirmation_delete_typography_scheme">Do you really want to delete the typography scheme %1$s?</string>
<string name="confirmation_delete_transparencies_scheme">Do you really want to delete the transparency scheme %1$s?</string>
<string name="new_theme_name">(untitled)</string>
<string name="theme_color_scheme_system_default">Use system default</string>

View File

@ -18,6 +18,8 @@ data class LauncherSettingsData internal constructor(
val uiShapesId: UUID = UUID(0L, 0L),
@Serializable(with = UUIDSerializer::class)
val uiTransparenciesId: UUID = UUID(0L, 0L),
@Serializable(with = UUIDSerializer::class)
val uiTypographyId: UUID = UUID(0L, 0L),
val uiCompatModeColors: Boolean = false,
val uiFont: Font = Font.Outfit,

View File

@ -318,6 +318,17 @@ class UiSettings internal constructor(
}
}
val typographyId
get() = launcherDataStore.data.map {
it.uiTypographyId
}.distinctUntilChanged()
fun setTypographyId(typographyId: UUID) {
launcherDataStore.update {
it.copy(uiTypographyId = typographyId)
}
}
val font
get() = launcherDataStore.data.map {
it.uiFont

View File

@ -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

View File

@ -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>)
}

View File

@ -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,
)

View File

@ -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()
)
}
}

View File

@ -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)

View File

@ -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())
}
}
}

View File

@ -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) {

View File

@ -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))
)

View File

@ -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(),
)
}
}

View File

@ -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
}

View File

@ -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,
)
}