From da8416a58c4a6e3cbd76fe871362851146f9b550 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Wed, 18 Jun 2025 22:28:13 +0200 Subject: [PATCH] (feat) custom typography schemes --- .../launcher2/ui/settings/SettingsActivity.kt | 12 + .../appearance/AppearanceSettingsScreen.kt | 24 +- .../appearance/AppearanceSettingsScreenVM.kt | 7 + .../appearance/ImportThemeSettingsScreen.kt | 16 +- .../colorscheme/ColorSchemesSettingsScreen.kt | 77 +- .../shapes/ShapeSchemesSettingsScreen.kt | 79 +- .../TransparencySchemesSettingsScreen.kt | 92 +- .../ui/settings/typography/PreviewTexts.kt | 88 ++ .../typography/TypographiesSettingsScreen.kt | 253 ++++ .../typography/TypographySettingsScreen.kt | 1098 +++++++++++++++++ .../typography/TypographySettingsScreenVM.kt | 50 + .../mm20/launcher2/ui/theme/LauncherTheme.kt | 16 +- .../ui/theme/colorscheme/ColorScheme.kt | 11 +- .../launcher2/ui/theme/typography/Common.kt | 2 +- .../ui/theme/typography/Typography.kt | 327 +++++ .../typography/fontfamily/DeviceDefault.kt | 37 + .../java/de/mm20/launcher2/icons/Icons.kt | 67 +- core/i18n/src/main/res/values/strings.xml | 2 + .../preferences/LauncherSettingsData.kt | 2 + .../launcher2/preferences/ui/UiSettings.kt | 11 + .../de/mm20/launcher2/database/AppDatabase.kt | 8 +- .../mm20/launcher2/database/daos/ThemeDao.kt | 22 + .../database/entities/TypographyEntity.kt | 43 + .../database/migrations/Migration_29_30.kt | 50 + .../de/mm20/launcher2/themes/DefaultThemes.kt | 11 +- .../de/mm20/launcher2/themes/Serialization.kt | 41 +- .../mm20/launcher2/themes/ThemeRepository.kt | 2 + .../launcher2/themes/typography/Defaults.kt | 127 ++ .../themes/typography/FontManager.kt | 45 + .../launcher2/themes/typography/Typography.kt | 228 ++++ .../themes/typography/TypographyRepository.kt | 125 ++ 31 files changed, 2856 insertions(+), 117 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/PreviewTexts.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographiesSettingsScreen.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreenVM.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Typography.kt create mode 100644 data/database/src/main/java/de/mm20/launcher2/database/entities/TypographyEntity.kt create mode 100644 data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_29_30.kt create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/typography/Defaults.kt create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/typography/FontManager.kt create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/typography/Typography.kt create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/typography/TypographyRepository.kt diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index 7ecd8347..097845ad 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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 { + TypographiesSettingsScreen() + } + composable { + val route: TypographySettingsRoute = it.toRoute() + ?: return@composable + TypographySettingsScreen(UUID.fromString(route.id)) + } composable("settings/appearance/cards") { CardsSettingsScreen() } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt index af7a892e..13b539ea 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt @@ -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, ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt index e0df7c2d..37689a75 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreenVM.kt @@ -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 { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/ImportThemeSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/ImportThemeSettingsScreen.kt index f2108b9e..6c6d5796 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/ImportThemeSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/ImportThemeSettingsScreen.kt @@ -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( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemesSettingsScreen.kt index 3a55ef8a..905c0062 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemesSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSchemesSettingsScreen.kt @@ -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(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) - } - ) + ) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemesSettingsScreen.kt index 13eb13c4..ff220d4a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemesSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/shapes/ShapeSchemesSettingsScreen.kt @@ -54,6 +54,8 @@ fun ShapeSchemesSettingsScreen() { var deleteShapes by remember { mutableStateOf(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) - } - ) + ) + } } } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/transparencies/TransparencySchemesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/transparencies/TransparencySchemesSettingsScreen.kt index 41865e06..14c207df 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/transparencies/TransparencySchemesSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/transparencies/TransparencySchemesSettingsScreen.kt @@ -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(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) ) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/PreviewTexts.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/PreviewTexts.kt new file mode 100644 index 00000000..9e69ef66 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/PreviewTexts.kt @@ -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" +} + + diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographiesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographiesSettingsScreen.kt new file mode 100644 index 00000000..2df177cc --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographiesSettingsScreen.kt @@ -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(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, + ) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt new file mode 100644 index 00000000..711e63a0 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreen.kt @@ -0,0 +1,1098 @@ +package de.mm20.launcher2.ui.settings.typography + +import android.content.Context +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FormatBold +import androidx.compose.material.icons.rounded.FormatLineSpacing +import androidx.compose.material.icons.rounded.FormatSize +import androidx.compose.material.icons.rounded.HorizontalDistribute +import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.LocaleList +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.icons.LetterSpacing2 +import de.mm20.launcher2.themes.typography.DefaultEmphasizedTextStyles +import de.mm20.launcher2.themes.typography.DefaultTextStyles +import de.mm20.launcher2.themes.typography.FontManager +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog +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.theme.typography.fontFamilyOf +import de.mm20.launcher2.ui.theme.typography.typographyOf +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import java.util.UUID +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.roundToInt +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 + +@Serializable +data class TypographySettingsRoute( + val id: String +) + +@Composable +fun TypographySettingsScreen(themeId: UUID) { + val viewModel: TypographySettingsScreenVM = viewModel() + + val context = LocalContext.current + + val theme by remember( + viewModel, + themeId + ) { viewModel.getTypography(themeId) }.collectAsStateWithLifecycle(null) + + val previewTypography = theme?.let { typographyOf(it) } + + var editName by remember { mutableStateOf(false) } + + if (editName) { + var name by remember(theme) { mutableStateOf(theme?.name ?: "") } + AlertDialog( + onDismissRequest = { editName = false }, + text = { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + singleLine = true + ) + }, + confirmButton = { + Button( + onClick = { + viewModel.updateTypography(theme!!.copy(name = name)) + editName = false + } + ) { + Text(stringResource(R.string.save)) + } + } + ) + } + + PreferenceScreen( + title = { + Text( + theme?.name ?: "", + modifier = Modifier.clickable { + editName = true + }, + ) + }, + helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/customization/color-schemes", + ) { + if (theme == null || previewTypography == null) return@PreferenceScreen + + item { + PreferenceCategory(title = "Fonts") { + FontPreference(title = "Brand", theme!!.fonts["brand"], onValueChange = { + viewModel.updateTypography( + theme!!.copy( + fonts = theme!!.fonts.toMutableMap().apply { put("brand", it) }) + ) + }) + FontPreference( + title = "Plain", + theme!!.fonts["plain"], + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + fonts = theme!!.fonts.toMutableMap().apply { put("plain", it) }) + ) + } + ) + } + } + item { + PreferenceCategory("Body") { + TextStylePreference( + title = "Body Small", + textStyle = previewTypography.bodySmall, + fonts = theme!!.fonts, + value = theme!!.styles.bodySmall, + defaultValue = DefaultTextStyles.bodySmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(bodySmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Body Medium", + textStyle = previewTypography.bodyMedium, + fonts = theme!!.fonts, + value = theme!!.styles.bodyMedium, + defaultValue = DefaultTextStyles.bodyMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(bodyMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Body Large", + textStyle = previewTypography.bodyLarge, + fonts = theme!!.fonts, + value = theme!!.styles.bodyLarge, + defaultValue = DefaultTextStyles.bodyLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(bodyLarge = it) + ) + ) + } + ) + TextStylePreference( + title = "Body Small Emphasized", + textStyle = previewTypography.bodySmallEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.bodySmall, + parentValue = theme!!.styles.bodySmall, + defaultValue = DefaultEmphasizedTextStyles.bodySmall, + defaultValueParent = DefaultTextStyles.bodySmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(bodySmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Body Medium Emphasized", + textStyle = previewTypography.bodyMediumEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.bodyMedium, + parentValue = theme!!.styles.bodyMedium, + defaultValue = DefaultEmphasizedTextStyles.bodyMedium, + defaultValueParent = DefaultTextStyles.bodyMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(bodyMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Body Large Emphasized", + textStyle = previewTypography.bodyLargeEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.bodyLarge, + parentValue = theme!!.styles.bodyLarge, + defaultValue = DefaultEmphasizedTextStyles.bodyLarge, + defaultValueParent = DefaultTextStyles.bodyLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(bodyLarge = it) + ) + ) + } + ) + } + } + item { + PreferenceCategory("Label") { + TextStylePreference( + title = "Label Small", + textStyle = previewTypography.labelSmall, + fonts = theme!!.fonts, + value = theme!!.styles.labelSmall, + defaultValue = DefaultTextStyles.labelSmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(labelSmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Label Medium", + textStyle = previewTypography.labelMedium, + fonts = theme!!.fonts, + value = theme!!.styles.labelMedium, + defaultValue = DefaultTextStyles.labelMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(labelMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Label Large", + textStyle = previewTypography.labelLarge, + fonts = theme!!.fonts, + value = theme!!.styles.labelLarge, + defaultValue = DefaultTextStyles.labelLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(labelLarge = it) + ) + ) + } + ) + TextStylePreference( + title = "Label Small Emphasized", + textStyle = previewTypography.labelSmallEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.labelSmall, + parentValue = theme!!.styles.labelSmall, + defaultValue = DefaultEmphasizedTextStyles.labelSmall, + defaultValueParent = DefaultTextStyles.labelSmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(labelSmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Label Medium Emphasized", + textStyle = previewTypography.labelMediumEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.labelMedium, + parentValue = theme!!.styles.labelMedium, + defaultValue = DefaultEmphasizedTextStyles.labelMedium, + defaultValueParent = DefaultTextStyles.labelMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(labelMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Label Large Emphasized", + textStyle = previewTypography.labelLargeEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.labelLarge, + parentValue = theme!!.styles.labelLarge, + defaultValue = DefaultEmphasizedTextStyles.labelLarge, + defaultValueParent = DefaultTextStyles.labelLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(labelLarge = it) + ) + ) + } + ) + } + } + item { + PreferenceCategory("Title") { + TextStylePreference( + title = "Title Small", + textStyle = previewTypography.titleSmall, + fonts = theme!!.fonts, + value = theme!!.styles.titleSmall, + defaultValue = DefaultTextStyles.titleSmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(titleSmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Title Medium", + textStyle = previewTypography.titleMedium, + fonts = theme!!.fonts, + value = theme!!.styles.titleMedium, + defaultValue = DefaultTextStyles.titleMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(titleMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Title Large", + textStyle = previewTypography.titleLarge, + fonts = theme!!.fonts, + value = theme!!.styles.titleLarge, + defaultValue = DefaultTextStyles.titleLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(titleLarge = it) + ) + ) + } + ) + TextStylePreference( + title = "Title Small Emphasized", + textStyle = previewTypography.titleSmallEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.titleSmall, + parentValue = theme!!.styles.titleSmall, + defaultValue = DefaultEmphasizedTextStyles.titleSmall, + defaultValueParent = DefaultTextStyles.titleSmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(titleSmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Title Medium Emphasized", + textStyle = previewTypography.titleMediumEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.titleMedium, + parentValue = theme!!.styles.titleMedium, + defaultValue = DefaultEmphasizedTextStyles.titleMedium, + defaultValueParent = DefaultTextStyles.titleMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(titleMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Title Large Emphasized", + textStyle = previewTypography.titleLargeEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.titleLarge, + parentValue = theme!!.styles.titleLarge, + defaultValue = DefaultEmphasizedTextStyles.titleLarge, + defaultValueParent = DefaultTextStyles.titleLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = theme!!.emphasizedStyles.copy(titleLarge = it) + ) + ) + } + ) + } + } + item { + PreferenceCategory("Headline") { + TextStylePreference( + title = "Headline Small", + textStyle = previewTypography.headlineSmall, + fonts = theme!!.fonts, + value = theme!!.styles.headlineSmall, + defaultValue = DefaultTextStyles.headlineSmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(headlineSmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Headline Medium", + textStyle = previewTypography.headlineMedium, + fonts = theme!!.fonts, + value = theme!!.styles.headlineMedium, + defaultValue = DefaultTextStyles.headlineMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(headlineMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Headline Large", + textStyle = previewTypography.headlineLarge, + fonts = theme!!.fonts, + value = theme!!.styles.headlineLarge, + defaultValue = DefaultTextStyles.headlineLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(headlineLarge = it) + ) + ) + } + ) + TextStylePreference( + title = "Headline Small Emphasized", + textStyle = previewTypography.headlineSmallEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.headlineSmall, + parentValue = theme!!.styles.headlineSmall, + defaultValue = DefaultEmphasizedTextStyles.headlineSmall, + defaultValueParent = DefaultTextStyles.headlineSmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = + theme!!.emphasizedStyles.copy(headlineSmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Headline Medium Emphasized", + textStyle = previewTypography.headlineMediumEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.headlineMedium, + parentValue = theme!!.styles.headlineMedium, + defaultValue = DefaultEmphasizedTextStyles.headlineMedium, + defaultValueParent = DefaultTextStyles.headlineMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = + theme!!.emphasizedStyles.copy(headlineMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Headline Large Emphasized", + textStyle = previewTypography.headlineLargeEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.headlineLarge, + parentValue = theme!!.styles.headlineLarge, + defaultValue = DefaultEmphasizedTextStyles.headlineLarge, + defaultValueParent = DefaultTextStyles.headlineLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = + theme!!.emphasizedStyles.copy(headlineLarge = it) + ) + ) + } + ) + } + } + item { + PreferenceCategory("Display") { + TextStylePreference( + title = "Display Small", + textStyle = previewTypography.displaySmall, + fonts = theme!!.fonts, + value = theme!!.styles.displaySmall, + defaultValue = DefaultTextStyles.displaySmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(displaySmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Display Medium", + textStyle = previewTypography.displayMedium, + fonts = theme!!.fonts, + value = theme!!.styles.displayMedium, + defaultValue = DefaultTextStyles.displayMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(displayMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Display Large", + textStyle = previewTypography.displayLarge, + fonts = theme!!.fonts, + value = theme!!.styles.displayLarge, + defaultValue = DefaultTextStyles.displayLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + styles = theme!!.styles.copy(displayLarge = it) + ) + ) + } + ) + TextStylePreference( + title = "Display Small Emphasized", + textStyle = previewTypography.displaySmallEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.displaySmall, + parentValue = theme!!.styles.displaySmall, + defaultValue = DefaultEmphasizedTextStyles.displaySmall, + defaultValueParent = DefaultTextStyles.displaySmall, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = + theme!!.emphasizedStyles.copy(displaySmall = it) + ) + ) + } + ) + TextStylePreference( + title = "Display Medium Emphasized", + textStyle = previewTypography.displayMediumEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.displayMedium, + parentValue = theme!!.styles.displayMedium, + defaultValue = DefaultEmphasizedTextStyles.displayMedium, + defaultValueParent = DefaultTextStyles.displayMedium, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = + theme!!.emphasizedStyles.copy(displayMedium = it) + ) + ) + } + ) + TextStylePreference( + title = "Display Large Emphasized", + textStyle = previewTypography.displayLargeEmphasized, + fonts = theme!!.fonts, + value = theme!!.emphasizedStyles.displayLarge, + parentValue = theme!!.styles.displayLarge, + defaultValue = DefaultEmphasizedTextStyles.displayLarge, + defaultValueParent = DefaultTextStyles.displayLarge, + onValueChange = { + viewModel.updateTypography( + theme!!.copy( + emphasizedStyles = + theme!!.emphasizedStyles.copy(displayLarge = it) + ) + ) + } + ) + } + } + } +} + +@Composable +private fun FontPreference( + title: String, + value: ThemeFontFamily?, + onValueChange: (ThemeFontFamily?) -> Unit = {}, +) { + val preview = PreviewTexts() + val context = LocalContext.current + val fontManager = FontManager(context) + + var showDialog by remember { mutableStateOf(false) } + + Preference( + title = title, + summary = getFontName(context, value), + icon = { + Text( + text = preview.ExtraShort, + style = TextStyle( + fontFamily = remember(value) { fontFamilyOf(context, value) }, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.primary, + fontSize = 24.sp, + textAlign = TextAlign.Center, + ) + ) + }, + onClick = { showDialog = true }, + ) + + if (showDialog) { + val sheetState = rememberModalBottomSheetState() + val fonts = remember { fontManager.getInstalledFonts() } + val scope = rememberCoroutineScope() + BottomSheetDialog( + onDismissRequest = { showDialog = false }, + bottomSheetState = sheetState + ) { + LazyColumn(contentPadding = it, verticalArrangement = Arrangement.spacedBy(12.dp)) { + if (fonts.builtIn.isNotEmpty()) { + item { + FontPickerCategory( + preview.ExtraShort, + null, + fonts.builtIn, + onFontClick = { + scope.launch { + sheetState.hide() + onValueChange(it) + showDialog = false + } + }) + } + } + if (fonts.deviceDefault.isNotEmpty()) { + item { + FontPickerCategory( + preview.ExtraShort, + "Device default", + fonts.deviceDefault, + onFontClick = { + scope.launch { + sheetState.hide() + onValueChange(it) + showDialog = false + } + }) + } + } + if (fonts.system.isNotEmpty()) { + item { + FontPickerCategory( + preview.ExtraShort, + "System", + fonts.system, + onFontClick = { + scope.launch { + sheetState.hide() + onValueChange(it) + showDialog = false + } + }) + } + } + if (fonts.generic.isNotEmpty()) { + item { + FontPickerCategory( + preview.ExtraShort, + "Generic", + fonts.generic, + onFontClick = { + scope.launch { + sheetState.hide() + onValueChange(it) + showDialog = false + } + }) + } + } + } + } + } +} + +@Composable +private fun FontPickerCategory( + previewText: String, categoryName: String?, + fonts: List, + onFontClick: (ThemeFontFamily?) -> Unit = {}, +) { + val context = LocalContext.current + PreferenceCategory(categoryName) { + for (font in fonts) { + val f = remember(font) { fontFamilyOf(context, font) } + Preference( + title = { + Text( + getFontName(context, font), + fontFamily = f + ) + }, + icon = { + Text( + text = previewText, + style = TextStyle( + fontFamily = f, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.primary, + fontSize = 24.sp, + textAlign = TextAlign.Center, + ) + ) + }, + summary = getFontSummary(context, font)?.let { { Text(it, fontFamily = f) } }, + onClick = { + onFontClick(font) + }, + ) + } + } +} + + +private fun getFontName(context: Context, fontFamily: ThemeFontFamily?): String { + return when (fontFamily) { + is ThemeFontFamily.LauncherDefault -> "Outfit" + is ThemeFontFamily.DeviceHeadline -> "Default headline font" + is ThemeFontFamily.DeviceBody -> "Default text font" + is ThemeFontFamily.System -> fontFamily.name + is ThemeFontFamily.SansSerif -> "sans-serif" + is ThemeFontFamily.Serif -> "serif" + is ThemeFontFamily.Monospace -> "monospace" + null -> "default" + } +} + +private fun getFontSummary(context: Context, fontFamily: ThemeFontFamily?): String? { + return when (fontFamily) { + is ThemeFontFamily.DeviceHeadline -> { + val resId = context.resources + .getIdentifier("config_headlineFontFamily", "string", "android") + if (resId != 0) return context.getString(resId) + return "sans-serif" + } + + is ThemeFontFamily.DeviceBody -> { + val resId = context.resources + .getIdentifier("config_bodyFontFamily", "string", "android") + if (resId != 0) return context.getString(resId) + return "sans-serif" + } + + else -> null + } +} + +@Composable +private fun SliderRow( + icon: ImageVector, + min: Float, + max: Float, + step: Float = 1f, + value: Float, + onValueChange: (Float) -> Unit, + formatValue: (Float) -> String, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + tint = MaterialTheme.colorScheme.primary + ) + Slider( + modifier = Modifier.weight(1f), + value = value, + onValueChange = onValueChange, + valueRange = min..max, + steps = ((max - min) / step).roundToInt() - 1, + ) + Text( + modifier = Modifier.width(32.dp), + text = formatValue(value), + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun TextStylePreference( + title: String, + textStyle: TextStyle, + fonts: Map, + value: ThemeTextStyle?, + parentValue: ThemeTextStyle? = null, + defaultValue: ThemeTextStyle?, + defaultValueParent: ThemeTextStyle? = null, + onValueChange: (ThemeTextStyle?) -> Unit = {}, +) { + val preview = PreviewTexts() + val context = LocalContext.current + + var showDialog by remember { mutableStateOf(false) } + + Preference( + title = { + Text( + title, + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) + ) + }, + icon = { + Text( + text = preview.ExtraShort, + style = textStyle, + color = MaterialTheme.colorScheme.primary, + maxLines = 1, + overflow = TextOverflow.Clip, + textAlign = TextAlign.Center, + ) + }, + onClick = { showDialog = true }, + ) + if (showDialog) { + var fontFamily by remember(value) { + mutableStateOf(value?.fontFamily) + } + var fontSize by remember(value) { + mutableStateOf(value?.fontSize) + } + var lineHeight by remember(value) { + mutableStateOf(value?.lineHeight) + } + var weight by remember(value) { + mutableStateOf((value?.fontWeight as? ThemeFontWeight.Absolute)?.weight) + } + var letterSpacing by remember(value) { + mutableStateOf(value?.letterSpacing) + } + + BottomSheetDialog( + onDismissRequest = { + onValueChange( + ThemeTextStyle( + fontFamily = fontFamily, + fontSize = fontSize, + lineHeight = lineHeight, + fontWeight = weight?.let { ThemeFontWeight.Absolute(it) }, + letterSpacing = letterSpacing + ) + ) + showDialog = false + }, + ) { + + + val actualFontFamily = fontFamily + ?: parentValue?.fontFamily + ?: defaultValue?.fontFamily + ?: defaultValueParent?.fontFamily!! + val actualFontSize = fontSize + ?: parentValue?.fontSize + ?: defaultValue?.fontSize + ?: defaultValueParent?.fontSize!! + val actualLineHeight = lineHeight + ?: parentValue?.lineHeight + ?: defaultValue?.lineHeight + ?: defaultValueParent?.lineHeight!! + val actualWeight = when { + weight != null -> FontWeight(weight!!) + parentValue?.fontWeight != null -> FontWeight(parentValue.fontWeight!!.weight) + defaultValue?.fontWeight is ThemeFontWeight.Absolute -> FontWeight((defaultValue.fontWeight as ThemeFontWeight.Absolute).weight) + defaultValue?.fontWeight is ThemeFontWeight.Relative && + defaultValueParent?.fontWeight is ThemeFontWeight.Absolute -> { + FontWeight( + (defaultValueParent.fontWeight as ThemeFontWeight.Absolute).weight + + (defaultValue.fontWeight as ThemeFontWeight.Relative).relativeWeight + ) + } + + else -> FontWeight.Normal + } + val actualLetterSpacing = letterSpacing + ?: parentValue?.letterSpacing + ?: defaultValue?.letterSpacing + ?: defaultValueParent?.letterSpacing!! + + Column( + modifier = Modifier.padding(it), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 200.dp), + contentAlignment = Alignment.Center, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = preview.TwoLines, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, + fontFamily = fontFamilyOf(context, fonts[actualFontFamily]), + fontSize = actualFontSize.sp, + lineHeight = actualLineHeight.em, + fontWeight = actualWeight, + letterSpacing = actualLetterSpacing.em, + ) + } + Row( + modifier = Modifier + .padding(vertical = 16.dp) + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + for ((name, font) in fonts) { + val f = remember(font) { fontFamilyOf(context, font) } + Column( + modifier = Modifier + .width(56.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + + Box( + modifier = Modifier + .size(56.dp) + .border( + if (name == actualFontFamily) 2.dp else 1.dp, + if (name == actualFontFamily) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant, + CircleShape + ) + .clip(CircleShape) + .clickable { + fontFamily = name + }, + contentAlignment = Alignment.Center, + ) { + Text( + text = preview.ExtraShort, + fontFamily = f, + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + ) + } + Text( + text = name.capitalize(LocaleList.current), + fontFamily = f, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = Modifier.width(56.dp), + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } + } + } + + SliderRow( + icon = Icons.Rounded.FormatBold, + min = 100f, + max = 900f, + step = 100f, + value = actualWeight.weight.toFloat(), + onValueChange = { weight = it.toInt() }, + formatValue = { + it.roundToInt().toString() + } + ) + SliderRow( + icon = Icons.Rounded.FormatSize, + min = floor((defaultValue?.fontSize ?: defaultValueParent?.fontSize)!! / 2f), + max = ceil((defaultValue?.fontSize ?: defaultValueParent?.fontSize)!! * 2f), + value = actualFontSize.toFloat(), + onValueChange = { + fontSize = it.roundToInt() + }, + formatValue = { + it.roundToInt().toString() + } + ) + SliderRow( + icon = Icons.Rounded.FormatLineSpacing, + min = 0.5f, + max = 2f, + step = 0.05f, + value = actualLineHeight, + onValueChange = { lineHeight = it }, + formatValue = { + (it * 100).roundToInt().toString() + "%" + } + ) + SliderRow( + icon = Icons.Rounded.LetterSpacing2, + min = -0.25f, + max = 1f, + step = 0.01f, + value = actualLetterSpacing, + onValueChange = { letterSpacing = (it * 100f).roundToInt() / 100f }, + formatValue = { + (it * 100).roundToInt().toString() + "%" + } + ) + + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp) + ) + + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + onClick = { + fontFamily = null + fontSize = null + lineHeight = null + weight = null + letterSpacing = null + onValueChange(null) + } + ) { + Icon( + Icons.Rounded.RestartAlt, null, + modifier = Modifier + .padding(ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text(stringResource(R.string.preference_restore_default)) + } + } + } + + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreenVM.kt new file mode 100644 index 00000000..086d9b43 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/typography/TypographySettingsScreenVM.kt @@ -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> = themeRepository.typographies.getAll() + + fun getTypography(id: UUID): Flow { + 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) + ) + ) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt index b991666e..f01d4556 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt @@ -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, diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/ColorScheme.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/ColorScheme.kt index e163338b..b8df9dfd 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/ColorScheme.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/ColorScheme.kt @@ -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 { 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(), ) } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt index 39a51292..e53a9b75 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Common.kt @@ -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), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Typography.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Typography.kt new file mode 100644 index 00000000..069dedde --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/Typography.kt @@ -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?, + fallback: ThemeTextStyle, + base: TextStyle, + fonts: Map, +): 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?, + parent: ThemeTextStyle?, + fallback: ThemeTextStyle, + fallbackParent: ThemeTextStyle, + base: TextStyle, + fonts: Map, +): 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 +): Map { + val distinct = fonts.values.distinct() + + val map: Map = 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 + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/DeviceDefault.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/DeviceDefault.kt index 2ddbecdb..b62a27cf 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/DeviceDefault.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/typography/fontfamily/DeviceDefault.kt @@ -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 } \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt b/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt index 4ab48fe6..7c8c647c 100644 --- a/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt +++ b/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt @@ -1834,4 +1834,69 @@ val _RoundedCornerAlt = materialIcon("Icons.Rounded.RoundedCornerAlt") { } val Icons.Rounded.RoundedCornerAlt - get() = _RoundedCornerAlt \ No newline at end of file + 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 \ No newline at end of file diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 20017ca3..77729e3c 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -439,6 +439,7 @@ Extra round Rectangular Base shape + Typography Transparency Default Semi-transparent @@ -824,6 +825,7 @@ 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. Do you really want to delete the color scheme %1$s\? Do you really want to delete the shape scheme %1$s? + Do you really want to delete the typography scheme %1$s? Do you really want to delete the transparency scheme %1$s? (untitled) Use system default diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index 83253847..8b0cd24c 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -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, diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt index 9abf8d16..2849c7ae 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/ui/UiSettings.kt @@ -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 diff --git a/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt index d49eff12..9ba30e99 100644 --- a/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -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 diff --git a/data/database/src/main/java/de/mm20/launcher2/database/daos/ThemeDao.kt b/data/database/src/main/java/de/mm20/launcher2/database/daos/ThemeDao.kt index 08b2f098..9a80bf4e 100644 --- a/data/database/src/main/java/de/mm20/launcher2/database/daos/ThemeDao.kt +++ b/data/database/src/main/java/de/mm20/launcher2/database/daos/ThemeDao.kt @@ -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> + @Query("SELECT * FROM Typography") + fun getAllTypographies(): Flow> + @Query("SELECT * FROM Theme WHERE id = :id LIMIT 1") fun getColors(id: UUID): Flow @@ -30,6 +34,9 @@ interface ThemeDao { @Query("SELECT * FROM Transparencies WHERE id = :id LIMIT 1") fun getTransparencies(id: UUID): Flow + @Query("SELECT * FROM Typography WHERE id = :id LIMIT 1") + fun getTypography(id: UUID): Flow + @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) @@ -74,4 +93,7 @@ interface ThemeDao { @Insert fun insertAllTransparencies(transparencies: List) + + @Insert + fun insertAllTypographies(typography: List) } \ No newline at end of file diff --git a/data/database/src/main/java/de/mm20/launcher2/database/entities/TypographyEntity.kt b/data/database/src/main/java/de/mm20/launcher2/database/entities/TypographyEntity.kt new file mode 100644 index 00000000..48a6ee83 --- /dev/null +++ b/data/database/src/main/java/de/mm20/launcher2/database/entities/TypographyEntity.kt @@ -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, +) \ No newline at end of file diff --git a/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_29_30.kt b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_29_30.kt new file mode 100644 index 00000000..7d3cae2a --- /dev/null +++ b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_29_30.kt @@ -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() + ) + } +} \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt index 0629e4ae..c1e00ac6 100644 --- a/data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt @@ -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) \ No newline at end of file +val SemiTransparentId = UUID(0L, 1L) + +val SystemFontId = UUID(0L, 1L) +val MonospaceId = UUID(0L, 2L) +val SerifId = UUID(0L, 3L) \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt index 1e470bac..c2bd2090 100644 --- a/data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/Serialization.kt @@ -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 { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING) +internal object ColorSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ColorSerializer", PrimitiveKind.STRING) override fun serialize( encoder: Encoder, @@ -42,8 +44,9 @@ internal object ColorSerializer: KSerializer { } } -internal object ShapeSerializer: KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ShapeSerializer", PrimitiveKind.STRING) +internal object ShapeSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ShapeSerializer", PrimitiveKind.STRING) override fun serialize( encoder: Encoder, @@ -55,4 +58,34 @@ internal object ShapeSerializer: KSerializer { override fun deserialize(decoder: Decoder): Shape { return Shape.fromString(decoder.decodeString())!! } +} + +internal object FontWeightSerializer : KSerializer { + 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()) + } + } } \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt index da9f6f05..5ac83cde 100644 --- a/data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt @@ -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) { diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/typography/Defaults.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/Defaults.kt new file mode 100644 index 00000000..174db0c0 --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/Defaults.kt @@ -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)) +) \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/typography/FontManager.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/FontManager.kt new file mode 100644 index 00000000..6a1d8a84 --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/FontManager.kt @@ -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, + val generic: List, + val deviceDefault: List, + val system: List, +) + +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(), + ) + } +} \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/typography/Typography.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/Typography.kt new file mode 100644 index 00000000..6312a67f --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/Typography.kt @@ -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 = mapOf("brand" to null, "plain" to null), + + val styles: TextStyles<@Contextual FontWeight.Absolute?> = TextStyles(), + val emphasizedStyles: TextStyles = 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( + val bodySmall: TextStyle? = null, + val bodyMedium: TextStyle? = null, + val bodyLarge: TextStyle? = null, + val labelSmall: TextStyle? = null, + val labelMedium: TextStyle? = null, + val labelLarge: TextStyle? = null, + val titleSmall: TextStyle? = null, + val titleMedium: TextStyle? = null, + val titleLarge: TextStyle? = null, + val headlineSmall: TextStyle? = null, + val headlineMedium: TextStyle? = null, + val headlineLarge: TextStyle? = null, + val displaySmall: TextStyle? = null, + val displayMedium: TextStyle? = null, + val displayLarge: TextStyle? = null, +) + +@Serializable +data class TextStyle( + /** + * 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>(this) + } + + companion object { + fun fromString(string: String?): TextStyle? { + if (string.isNullOrEmpty()) return null + return ThemeJson.decodeFromString>(string) as TextStyle + } + } +} + +@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 +} \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/typography/TypographyRepository.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/TypographyRepository.kt new file mode 100644 index 00000000..a723013e --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/typography/TypographyRepository.kt @@ -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> { + return database.themeDao().getAllTypographies().map { + getBuiltIn() + it.map { Typography(it) } + } + } + + fun get(id: UUID): Flow { + 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 { + if (id == null) return flowOf(default) + return get(id).map { it ?: default } + } + + private fun getBuiltIn(): List { + 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, + ) + +} \ No newline at end of file