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