feat(theme): user-selectable custom fonts (#19)
Add a per-role font picker in Appearance settings. The two Material typeface roles can be set independently: brand (display/headline) and plain (title/body/label). Each can be the system default, a bundled font (Atkinson Hyperlegible, Lora, JetBrains Mono), or a .ttf/.otf the user loads from the device. The choice rebuilds the app-wide typography in MainActivity; a null/"system" choice leaves the Material default. Per the issue discussion the font-size and font-colour parts were dropped — Android's accessibility font scaling and the dynamic-colour theme already cover them. Custom files are copied into app-private storage and validated with android.graphics.fonts.Font.Builder before replacing the previous one, so a bad pick can't wedge global text rendering; a missing/unreadable file degrades to the system font. Each bundled font is previewed in its own face in the picker. OFL licences + attribution vendored under licenses/fonts/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,8 +32,11 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity
|
import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity
|
||||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
||||||
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
|
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
|
||||||
|
import de.jeanlucmakiola.calendula.domain.FontRole
|
||||||
import de.jeanlucmakiola.calendula.ui.settings.SettingsViewModel
|
import de.jeanlucmakiola.calendula.ui.settings.SettingsViewModel
|
||||||
import de.jeanlucmakiola.calendula.ui.theme.CalendulaTheme
|
import de.jeanlucmakiola.calendula.ui.theme.CalendulaTheme
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.calendulaTypography
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
@@ -97,9 +100,20 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val use24Hour = remember(settings.timeFormat, context) {
|
val use24Hour = remember(settings.timeFormat, context) {
|
||||||
settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(context))
|
settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(context))
|
||||||
}
|
}
|
||||||
|
// The user's custom-font choice, resolved to a Material typography
|
||||||
|
// (issue #19). Recomputed only when a token changes; "system for both"
|
||||||
|
// returns the default scale untouched.
|
||||||
|
val fonts by settingsViewModel.fontState.collectAsStateWithLifecycle()
|
||||||
|
val typography = remember(fonts, context) {
|
||||||
|
calendulaTypography(
|
||||||
|
brand = resolveFontFamily(fonts.brand, FontRole.BRAND, context),
|
||||||
|
plain = resolveFontFamily(fonts.plain, FontRole.PLAIN, context),
|
||||||
|
)
|
||||||
|
}
|
||||||
CalendulaTheme(
|
CalendulaTheme(
|
||||||
darkTheme = darkTheme,
|
darkTheme = darkTheme,
|
||||||
dynamicColor = settings.dynamicColor,
|
dynamicColor = settings.dynamicColor,
|
||||||
|
typography = typography,
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalUse24HourFormat provides use24Hour,
|
LocalUse24HourFormat provides use24Hour,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.data.fonts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import de.jeanlucmakiola.calendula.domain.FontRole
|
||||||
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
import android.graphics.fonts.Font as PlatformFont
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage for user-loaded custom fonts (issue #19). A font a user picks via the
|
||||||
|
* system file picker is copied into the app's private storage — one file per
|
||||||
|
* [FontRole] — so the selection survives even if the original is later moved or
|
||||||
|
* deleted, and never leaves the app. The chosen file is validated as a real font
|
||||||
|
* before it replaces the previous one, so a bad pick can't wedge the app-wide
|
||||||
|
* typography.
|
||||||
|
*/
|
||||||
|
object CustomFontStore {
|
||||||
|
|
||||||
|
private fun dir(context: Context): File = File(context.filesDir, "fonts")
|
||||||
|
|
||||||
|
/** The stored font file for [role] (may not exist yet). */
|
||||||
|
fun file(context: Context, role: FontRole): File =
|
||||||
|
File(dir(context), "${role.name.lowercase(Locale.ROOT)}.ttf")
|
||||||
|
|
||||||
|
fun exists(context: Context, role: FontRole): Boolean =
|
||||||
|
file(context, role).let { it.exists() && it.length() > 0 }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy [uri] into per-role storage, first validating that it parses as a
|
||||||
|
* font. Returns true on success; on any failure the previous file is left
|
||||||
|
* untouched and false is returned (the caller keeps the old selection).
|
||||||
|
*/
|
||||||
|
fun import(context: Context, role: FontRole, uri: Uri): Boolean {
|
||||||
|
val target = file(context, role)
|
||||||
|
target.parentFile?.mkdirs()
|
||||||
|
val tmp = File(target.parentFile, "${role.name.lowercase(Locale.ROOT)}.tmp")
|
||||||
|
return try {
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
tmp.outputStream().use { output -> input.copyTo(output) }
|
||||||
|
} ?: return false
|
||||||
|
// Font.Builder throws IOException for anything that isn't a valid
|
||||||
|
// font file (API 29+, which is our minSdk) — a cheap, reliable check.
|
||||||
|
PlatformFont.Builder(tmp).build()
|
||||||
|
if (target.exists() && !target.delete()) return false
|
||||||
|
tmp.renameTo(target)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
tmp.delete()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forget the custom font for [role] (used when switching away from it). */
|
||||||
|
fun clear(context: Context, role: FontRole) {
|
||||||
|
file(context, role).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
|||||||
import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange
|
import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
@@ -104,6 +105,27 @@ class SettingsPrefs @Inject constructor(
|
|||||||
store.edit { it[DYNAMIC_COLOR_KEY] = enabled }
|
store.edit { it[DYNAMIC_COLOR_KEY] = enabled }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom-font tokens per Material typeface role (issue #19). Stored as opaque
|
||||||
|
* strings — "system", "custom", or a bundled font's token — resolved to a
|
||||||
|
* FontFamily at the theme layer; an unknown token degrades to the default.
|
||||||
|
*/
|
||||||
|
val brandFont: Flow<String> = store.data.map { prefs ->
|
||||||
|
prefs[BRAND_FONT_KEY] ?: FONT_SYSTEM_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
val plainFont: Flow<String> = store.data.map { prefs ->
|
||||||
|
prefs[PLAIN_FONT_KEY] ?: FONT_SYSTEM_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setBrandFont(token: String) {
|
||||||
|
store.edit { it[BRAND_FONT_KEY] = token }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setPlainFont(token: String) {
|
||||||
|
store.edit { it[PLAIN_FONT_KEY] = token }
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun setWeekStart(pref: WeekStartPref) {
|
suspend fun setWeekStart(pref: WeekStartPref) {
|
||||||
store.edit { it[WEEK_START_KEY] = pref.storageValue() }
|
store.edit { it[WEEK_START_KEY] = pref.storageValue() }
|
||||||
}
|
}
|
||||||
@@ -436,6 +458,8 @@ class SettingsPrefs @Inject constructor(
|
|||||||
companion object {
|
companion object {
|
||||||
internal val THEME_MODE_KEY = stringPreferencesKey("theme_mode")
|
internal val THEME_MODE_KEY = stringPreferencesKey("theme_mode")
|
||||||
internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color")
|
internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color")
|
||||||
|
internal val BRAND_FONT_KEY = stringPreferencesKey("brand_font")
|
||||||
|
internal val PLAIN_FONT_KEY = stringPreferencesKey("plain_font")
|
||||||
internal val WEEK_START_KEY = stringPreferencesKey("week_start")
|
internal val WEEK_START_KEY = stringPreferencesKey("week_start")
|
||||||
internal val AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_range")
|
internal val AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_range")
|
||||||
internal val AGENDA_WIDGET_RANGE_KEY = stringPreferencesKey("agenda_widget_range")
|
internal val AGENDA_WIDGET_RANGE_KEY = stringPreferencesKey("agenda_widget_range")
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.domain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two Material 3 typeface roles a user can set independently (issue #19):
|
||||||
|
* [BRAND] drives the display/headline styles (expression), [PLAIN] the
|
||||||
|
* title/body/label styles (readability). Each defaults to the system typeface.
|
||||||
|
*/
|
||||||
|
enum class FontRole { BRAND, PLAIN }
|
||||||
@@ -7,9 +7,11 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -51,6 +53,7 @@ import androidx.compose.material.icons.filled.Notifications
|
|||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
import androidx.compose.material.icons.filled.Translate
|
import androidx.compose.material.icons.filled.Translate
|
||||||
import androidx.compose.material.icons.filled.Tune
|
import androidx.compose.material.icons.filled.Tune
|
||||||
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -60,6 +63,7 @@ import androidx.compose.material3.Switch
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -74,6 +78,7 @@ import androidx.compose.ui.res.colorResource
|
|||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -95,9 +100,11 @@ import de.jeanlucmakiola.calendula.qs.NewEventTileService
|
|||||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
||||||
import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker
|
import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker
|
||||||
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
|
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
|
||||||
|
import de.jeanlucmakiola.calendula.domain.FontRole
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit
|
import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter
|
import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter
|
||||||
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.FullScreenPicker
|
||||||
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
||||||
@@ -115,6 +122,10 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
|
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel
|
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.BundledFont
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
import kotlinx.datetime.LocalTime
|
import kotlinx.datetime.LocalTime
|
||||||
import java.time.format.TextStyle as JavaTextStyle
|
import java.time.format.TextStyle as JavaTextStyle
|
||||||
@@ -477,6 +488,18 @@ private fun AppearanceScreen(
|
|||||||
var showAgendaScreenRange by remember { mutableStateOf(false) }
|
var showAgendaScreenRange by remember { mutableStateOf(false) }
|
||||||
var showAgendaWidgetRange by remember { mutableStateOf(false) }
|
var showAgendaWidgetRange by remember { mutableStateOf(false) }
|
||||||
var showPastEvents by remember { mutableStateOf(false) }
|
var showPastEvents by remember { mutableStateOf(false) }
|
||||||
|
var showBrandFont by remember { mutableStateOf(false) }
|
||||||
|
var showPlainFont by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val fonts by viewModel.fontState.collectAsStateWithLifecycle()
|
||||||
|
// A picked file that didn't parse as a font: tell the user and keep the old choice.
|
||||||
|
val context = LocalContext.current
|
||||||
|
val importFailedMessage = stringResource(R.string.settings_font_import_failed)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.fontImportFailed.collect {
|
||||||
|
Toast.makeText(context, importFailedMessage, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CollapsingScaffold(
|
CollapsingScaffold(
|
||||||
title = stringResource(R.string.settings_section_appearance),
|
title = stringResource(R.string.settings_section_appearance),
|
||||||
@@ -513,6 +536,24 @@ private fun AppearanceScreen(
|
|||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Fonts — the two Material typeface roles, each independently choosable
|
||||||
|
// (issue #19). Headings = brand (display/headline); body = plain
|
||||||
|
// (title/body/label). Both default to the system typeface.
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.settings_font_headings),
|
||||||
|
summary = fontLabel(fonts.brand),
|
||||||
|
position = Position.Top,
|
||||||
|
onClick = { showBrandFont = true },
|
||||||
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.settings_font_body),
|
||||||
|
summary = fontLabel(fonts.plain),
|
||||||
|
position = Position.Bottom,
|
||||||
|
onClick = { showPlainFont = true },
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
// Calendar — view, week, and timeline formatting
|
// Calendar — view, week, and timeline formatting
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
title = stringResource(R.string.settings_default_view),
|
title = stringResource(R.string.settings_default_view),
|
||||||
@@ -604,6 +645,26 @@ private fun AppearanceScreen(
|
|||||||
onDismiss = { showTheme = false },
|
onDismiss = { showTheme = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (showBrandFont) {
|
||||||
|
FontPicker(
|
||||||
|
title = stringResource(R.string.settings_font_headings),
|
||||||
|
role = FontRole.BRAND,
|
||||||
|
selected = fonts.brand,
|
||||||
|
onSelect = { viewModel.setFont(FontRole.BRAND, it) },
|
||||||
|
onImport = { viewModel.importCustomFont(FontRole.BRAND, it) },
|
||||||
|
onDismiss = { showBrandFont = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showPlainFont) {
|
||||||
|
FontPicker(
|
||||||
|
title = stringResource(R.string.settings_font_body),
|
||||||
|
role = FontRole.PLAIN,
|
||||||
|
selected = fonts.plain,
|
||||||
|
onSelect = { viewModel.setFont(FontRole.PLAIN, it) },
|
||||||
|
onImport = { viewModel.importCustomFont(FontRole.PLAIN, it) },
|
||||||
|
onDismiss = { showPlainFont = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
if (showWeekStart) {
|
if (showWeekStart) {
|
||||||
OptionPicker(
|
OptionPicker(
|
||||||
title = stringResource(R.string.settings_week_start),
|
title = stringResource(R.string.settings_week_start),
|
||||||
@@ -1180,6 +1241,128 @@ private fun themeLabel(mode: ThemeMode): String = stringResource(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** The summary label for a stored font token (issue #19). */
|
||||||
|
@Composable
|
||||||
|
private fun fontLabel(token: String): String = when (token) {
|
||||||
|
FONT_SYSTEM_TOKEN -> stringResource(R.string.settings_font_system)
|
||||||
|
FONT_CUSTOM_TOKEN -> stringResource(R.string.settings_font_custom_selected)
|
||||||
|
else -> BundledFont.fromToken(token)?.let { stringResource(it.labelRes) }
|
||||||
|
?: stringResource(R.string.settings_font_system)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-screen font chooser for one [FontRole]: the system default, each bundled
|
||||||
|
* font (previewed in its own face), and "Choose file…" which opens the system
|
||||||
|
* picker to load a .ttf/.otf. Selecting a system/bundled option applies at once;
|
||||||
|
* a file is validated and imported by the caller, switching to the custom font
|
||||||
|
* on success.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun FontPicker(
|
||||||
|
title: String,
|
||||||
|
role: FontRole,
|
||||||
|
selected: String,
|
||||||
|
onSelect: (String) -> Unit,
|
||||||
|
onImport: (Uri) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
onImport(uri)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System default + the bundled fonts + the "Choose file…" row.
|
||||||
|
val rowCount = BundledFont.entries.size + 2
|
||||||
|
val isCustom = selected == FONT_CUSTOM_TOKEN
|
||||||
|
|
||||||
|
FullScreenPicker(title = title, onDismiss = onDismiss) {
|
||||||
|
FontOptionRow(
|
||||||
|
label = stringResource(R.string.settings_font_system),
|
||||||
|
preview = FontFamily.Default,
|
||||||
|
selected = selected == FONT_SYSTEM_TOKEN,
|
||||||
|
position = positionOf(0, rowCount),
|
||||||
|
onClick = {
|
||||||
|
onSelect(FONT_SYSTEM_TOKEN)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BundledFont.entries.forEachIndexed { index, font ->
|
||||||
|
FontOptionRow(
|
||||||
|
label = stringResource(font.labelRes),
|
||||||
|
preview = font.family,
|
||||||
|
selected = selected == font.token,
|
||||||
|
position = positionOf(index + 1, rowCount),
|
||||||
|
onClick = {
|
||||||
|
onSelect(font.token)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FontOptionRow(
|
||||||
|
label = if (isCustom) {
|
||||||
|
stringResource(R.string.settings_font_custom_selected)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.settings_font_choose_file)
|
||||||
|
},
|
||||||
|
// A loaded font previews in its own face; otherwise show an upload cue.
|
||||||
|
preview = if (isCustom) resolveFontFamily(FONT_CUSTOM_TOKEN, role, context) else null,
|
||||||
|
leadingIcon = if (isCustom) null else Icons.Default.UploadFile,
|
||||||
|
selected = isCustom,
|
||||||
|
position = positionOf(rowCount - 1, rowCount),
|
||||||
|
onClick = { launcher.launch(arrayOf("*/*")) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row in the [FontPicker]: the font's name, a leading "Ag" sample rendered in
|
||||||
|
* the option's own [preview] face (or an [leadingIcon] cue when there's nothing
|
||||||
|
* to preview), and a check when it's the current selection.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun FontOptionRow(
|
||||||
|
label: String,
|
||||||
|
preview: FontFamily?,
|
||||||
|
selected: Boolean,
|
||||||
|
position: Position,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
leadingIcon: ImageVector? = null,
|
||||||
|
) {
|
||||||
|
GroupedRow(
|
||||||
|
title = label,
|
||||||
|
position = position,
|
||||||
|
selected = selected,
|
||||||
|
leading = {
|
||||||
|
if (preview != null) {
|
||||||
|
Text(
|
||||||
|
text = "Ag",
|
||||||
|
fontFamily = preview,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
)
|
||||||
|
} else if (leadingIcon != null) {
|
||||||
|
Icon(imageVector = leadingIcon, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailing = if (selected) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Picker options: "Follow system" first, then Monday…Sunday in ISO order. */
|
/** Picker options: "Follow system" first, then Monday…Sunday in ISO order. */
|
||||||
private val WEEK_START_OPTIONS: List<WeekStartPref> =
|
private val WEEK_START_OPTIONS: List<WeekStartPref> =
|
||||||
listOf(WeekStartPref.Auto) + DayOfWeek.entries.map { WeekStartPref.Day(it) }
|
listOf(WeekStartPref.Auto) + DayOfWeek.entries.map { WeekStartPref.Day(it) }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.settings
|
package de.jeanlucmakiola.calendula.ui.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
@@ -10,6 +11,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
||||||
|
import de.jeanlucmakiola.calendula.data.di.IoDispatcher
|
||||||
|
import de.jeanlucmakiola.calendula.data.fonts.CustomFontStore
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||||
@@ -18,14 +21,20 @@ import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
|||||||
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
|
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
|
||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.domain.EventFormField
|
import de.jeanlucmakiola.calendula.domain.EventFormField
|
||||||
|
import de.jeanlucmakiola.calendula.domain.FontRole
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.AppFontSettings
|
||||||
|
import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN
|
||||||
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY
|
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY
|
||||||
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY
|
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY
|
||||||
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
|
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
|
||||||
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
|
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
@@ -35,12 +44,14 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel @Inject constructor(
|
class SettingsViewModel @Inject constructor(
|
||||||
private val prefs: SettingsPrefs,
|
private val prefs: SettingsPrefs,
|
||||||
repository: CalendarRepository,
|
repository: CalendarRepository,
|
||||||
|
@IoDispatcher private val io: CoroutineDispatcher,
|
||||||
@ApplicationContext private val appContext: Context,
|
@ApplicationContext private val appContext: Context,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -131,6 +142,27 @@ class SettingsViewModel @Inject constructor(
|
|||||||
initialValue = SettingsUiState(dynamicColorAvailable = dynamicColorAvailable),
|
initialValue = SettingsUiState(dynamicColorAvailable = dynamicColorAvailable),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app-wide custom-font choice (issue #19). Its own flow — both this
|
||||||
|
* screen and [MainActivity] (which builds the typography) read it
|
||||||
|
* independently of the main settings combine.
|
||||||
|
*/
|
||||||
|
val fontState: StateFlow<AppFontSettings> = combine(
|
||||||
|
prefs.brandFont,
|
||||||
|
prefs.plainFont,
|
||||||
|
) { brand, plain ->
|
||||||
|
AppFontSettings(brand = brand, plain = plain)
|
||||||
|
}.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000L),
|
||||||
|
initialValue = AppFontSettings(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Emitted when a picked font file couldn't be read as a font; the screen
|
||||||
|
// surfaces it and the previous selection stays put.
|
||||||
|
private val _fontImportFailed = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
val fontImportFailed: SharedFlow<Unit> = _fontImportFailed
|
||||||
|
|
||||||
// Serialises widget redraws so a rapid flip-and-flip-back can't run two
|
// Serialises widget redraws so a rapid flip-and-flip-back can't run two
|
||||||
// updateAll calls at once — concurrent calls coalesce around a stale read
|
// updateAll calls at once — concurrent calls coalesce around a stale read
|
||||||
// and can leave the widget on the intermediate value. Held sequentially,
|
// and can leave the widget on the intermediate value. Held sequentially,
|
||||||
@@ -174,6 +206,31 @@ class SettingsViewModel @Inject constructor(
|
|||||||
viewModelScope.launch { prefs.setDynamicColor(enabled) }
|
viewModelScope.launch { prefs.setDynamicColor(enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setFont(role: FontRole, token: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
when (role) {
|
||||||
|
FontRole.BRAND -> prefs.setBrandFont(token)
|
||||||
|
FontRole.PLAIN -> prefs.setPlainFont(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a user-picked font file for [role]. Copies and validates it off the
|
||||||
|
* main thread; on success the role switches to the custom font, otherwise the
|
||||||
|
* previous choice is kept and [fontImportFailed] fires.
|
||||||
|
*/
|
||||||
|
fun importCustomFont(role: FontRole, uri: Uri) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val ok = withContext(io) { CustomFontStore.import(appContext, role, uri) }
|
||||||
|
if (ok) {
|
||||||
|
setFont(role, FONT_CUSTOM_TOKEN)
|
||||||
|
} else {
|
||||||
|
_fontImportFailed.emit(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setWeekStart(pref: WeekStartPref) {
|
fun setWeekStart(pref: WeekStartPref) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
prefs.setWeekStart(pref)
|
prefs.setWeekStart(pref)
|
||||||
|
|||||||
124
app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt
Normal file
124
app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.theme
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontVariation
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
import de.jeanlucmakiola.calendula.data.fonts.CustomFontStore
|
||||||
|
import de.jeanlucmakiola.calendula.domain.FontRole
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/** Stored token for "use the device default typeface" — the default for both roles. */
|
||||||
|
const val FONT_SYSTEM_TOKEN = "system"
|
||||||
|
|
||||||
|
/** Stored token for "use the font the user loaded from a file" (per role). */
|
||||||
|
const val FONT_CUSTOM_TOKEN = "custom"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A font checked into the app that a user can pick without providing a file
|
||||||
|
* (issue #19). [token] is the stable value persisted in preferences; [family] is
|
||||||
|
* the resolved Compose family. Kept deliberately small and readability-focused.
|
||||||
|
*/
|
||||||
|
enum class BundledFont(
|
||||||
|
val token: String,
|
||||||
|
@StringRes val labelRes: Int,
|
||||||
|
val family: FontFamily,
|
||||||
|
) {
|
||||||
|
/** Purpose-built for low-vision legibility (Braille Institute). */
|
||||||
|
AtkinsonHyperlegible(
|
||||||
|
token = "atkinson_hyperlegible",
|
||||||
|
labelRes = R.string.font_atkinson_hyperlegible,
|
||||||
|
family = FontFamily(
|
||||||
|
Font(R.font.atkinson_hyperlegible_regular, FontWeight.Normal),
|
||||||
|
Font(R.font.atkinson_hyperlegible_bold, FontWeight.Bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/** A serif alternative; shipped as a variable font, weights via its wght axis. */
|
||||||
|
Lora(
|
||||||
|
token = "lora",
|
||||||
|
labelRes = R.string.font_lora,
|
||||||
|
family = FontFamily(
|
||||||
|
Font(R.font.lora, FontWeight.Normal, variationSettings = weightAxis(400)),
|
||||||
|
Font(R.font.lora, FontWeight.Medium, variationSettings = weightAxis(500)),
|
||||||
|
Font(R.font.lora, FontWeight.Bold, variationSettings = weightAxis(700)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/** A monospace option. */
|
||||||
|
JetBrainsMono(
|
||||||
|
token = "jetbrains_mono",
|
||||||
|
labelRes = R.string.font_jetbrains_mono,
|
||||||
|
family = FontFamily(
|
||||||
|
Font(R.font.jetbrains_mono_regular, FontWeight.Normal),
|
||||||
|
Font(R.font.jetbrains_mono_bold, FontWeight.Bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromToken(token: String?): BundledFont? = entries.firstOrNull { it.token == token }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun weightAxis(weight: Int) = FontVariation.Settings(FontVariation.weight(weight))
|
||||||
|
|
||||||
|
/** The two roles' current font tokens (see [FontRole]); the app-wide typeface choice. */
|
||||||
|
data class AppFontSettings(
|
||||||
|
val brand: String = FONT_SYSTEM_TOKEN,
|
||||||
|
val plain: String = FONT_SYSTEM_TOKEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a stored [token] for [role] to a Compose [FontFamily], or null to mean
|
||||||
|
* "leave the Material default" (the system typeface). A custom token whose file
|
||||||
|
* is missing or unreadable also resolves to null, so a deleted file degrades to
|
||||||
|
* the system font rather than crashing text layout.
|
||||||
|
*/
|
||||||
|
fun resolveFontFamily(token: String, role: FontRole, context: Context): FontFamily? = when (token) {
|
||||||
|
FONT_SYSTEM_TOKEN -> null
|
||||||
|
FONT_CUSTOM_TOKEN -> customFontFamily(CustomFontStore.file(context, role))
|
||||||
|
else -> BundledFont.fromToken(token)?.family
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun customFontFamily(file: File): FontFamily? =
|
||||||
|
if (file.exists() && file.length() > 0) {
|
||||||
|
runCatching { FontFamily(Font(file)) }.getOrNull()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the app typography from the chosen [brand] and [plain] families. A null
|
||||||
|
* family leaves that role's Material default (system typeface) in place, so the
|
||||||
|
* common "system for both" case returns [CalendulaTypography] untouched. Brand
|
||||||
|
* drives display + headline; plain drives title + body + label (M3's two
|
||||||
|
* typeface roles).
|
||||||
|
*/
|
||||||
|
fun calendulaTypography(brand: FontFamily?, plain: FontFamily?): Typography {
|
||||||
|
if (brand == null && plain == null) return CalendulaTypography
|
||||||
|
val base = CalendulaTypography
|
||||||
|
fun TextStyle.withFamily(family: FontFamily?): TextStyle =
|
||||||
|
if (family != null) copy(fontFamily = family) else this
|
||||||
|
return base.copy(
|
||||||
|
displayLarge = base.displayLarge.withFamily(brand),
|
||||||
|
displayMedium = base.displayMedium.withFamily(brand),
|
||||||
|
displaySmall = base.displaySmall.withFamily(brand),
|
||||||
|
headlineLarge = base.headlineLarge.withFamily(brand),
|
||||||
|
headlineMedium = base.headlineMedium.withFamily(brand),
|
||||||
|
headlineSmall = base.headlineSmall.withFamily(brand),
|
||||||
|
titleLarge = base.titleLarge.withFamily(plain),
|
||||||
|
titleMedium = base.titleMedium.withFamily(plain),
|
||||||
|
titleSmall = base.titleSmall.withFamily(plain),
|
||||||
|
bodyLarge = base.bodyLarge.withFamily(plain),
|
||||||
|
bodyMedium = base.bodyMedium.withFamily(plain),
|
||||||
|
bodySmall = base.bodySmall.withFamily(plain),
|
||||||
|
labelLarge = base.labelLarge.withFamily(plain),
|
||||||
|
labelMedium = base.labelMedium.withFamily(plain),
|
||||||
|
labelSmall = base.labelSmall.withFamily(plain),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialExpressiveTheme
|
import androidx.compose.material3.MaterialExpressiveTheme
|
||||||
import androidx.compose.material3.MotionScheme
|
import androidx.compose.material3.MotionScheme
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -15,15 +16,18 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
* - System light/dark.
|
* - System light/dark.
|
||||||
* - Dynamic Color on API 31+, else falls back to the hand-tuned scheme
|
* - Dynamic Color on API 31+, else falls back to the hand-tuned scheme
|
||||||
* derived from [CalendulaSeed].
|
* derived from [CalendulaSeed].
|
||||||
|
* - A user-chosen [typography] (custom fonts, issue #19), defaulting to the
|
||||||
|
* Material scale on the system typeface.
|
||||||
*
|
*
|
||||||
* The Settings screen (later) can override useDynamicColor and themePreference,
|
* The Settings screen overrides darkTheme, dynamicColor, and typography; the
|
||||||
* but the V1 foundation just follows the system.
|
* bare defaults (used by the crash screen) just follow the system.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CalendulaTheme(
|
fun CalendulaTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = true,
|
||||||
|
typography: Typography = CalendulaTypography,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
@@ -42,7 +46,7 @@ fun CalendulaTheme(
|
|||||||
// the bouncy variant felt overdone in review (2026-06-11).
|
// the bouncy variant felt overdone in review (2026-06-11).
|
||||||
MaterialExpressiveTheme(
|
MaterialExpressiveTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = CalendulaTypography,
|
typography = typography,
|
||||||
motionScheme = MotionScheme.standard(),
|
motionScheme = MotionScheme.standard(),
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
|||||||
BIN
app/src/main/res/font/atkinson_hyperlegible_bold.ttf
Normal file
BIN
app/src/main/res/font/atkinson_hyperlegible_bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/atkinson_hyperlegible_regular.ttf
Normal file
BIN
app/src/main/res/font/atkinson_hyperlegible_regular.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/jetbrains_mono_bold.ttf
Normal file
BIN
app/src/main/res/font/jetbrains_mono_bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/jetbrains_mono_regular.ttf
Normal file
BIN
app/src/main/res/font/jetbrains_mono_regular.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/lora.ttf
Normal file
BIN
app/src/main/res/font/lora.ttf
Normal file
Binary file not shown.
@@ -252,6 +252,15 @@
|
|||||||
<string name="settings_theme_dark">Dunkel</string>
|
<string name="settings_theme_dark">Dunkel</string>
|
||||||
<string name="settings_dynamic_color">Dynamische Farben</string>
|
<string name="settings_dynamic_color">Dynamische Farben</string>
|
||||||
<string name="settings_dynamic_color_unavailable">Erfordert Android 12 oder neuer</string>
|
<string name="settings_dynamic_color_unavailable">Erfordert Android 12 oder neuer</string>
|
||||||
|
<string name="settings_font_headings">Schriftart für Überschriften</string>
|
||||||
|
<string name="settings_font_body">Schriftart für Text</string>
|
||||||
|
<string name="settings_font_system">Systemstandard</string>
|
||||||
|
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
|
||||||
|
<string name="font_lora">Lora</string>
|
||||||
|
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||||
|
<string name="settings_font_choose_file">Datei auswählen…</string>
|
||||||
|
<string name="settings_font_custom_selected">Eigene Schriftart</string>
|
||||||
|
<string name="settings_font_import_failed">Diese Datei konnte nicht als Schriftart gelesen werden</string>
|
||||||
<string name="settings_week_start">Wochenstart</string>
|
<string name="settings_week_start">Wochenstart</string>
|
||||||
<string name="settings_week_start_auto">Automatisch</string>
|
<string name="settings_week_start_auto">Automatisch</string>
|
||||||
<string name="settings_time_format">Zeitformat</string>
|
<string name="settings_time_format">Zeitformat</string>
|
||||||
|
|||||||
@@ -274,6 +274,15 @@
|
|||||||
<string name="settings_default_view">Default view</string>
|
<string name="settings_default_view">Default view</string>
|
||||||
<string name="settings_dynamic_color">Dynamic colour</string>
|
<string name="settings_dynamic_color">Dynamic colour</string>
|
||||||
<string name="settings_dynamic_color_unavailable">Requires Android 12 or newer</string>
|
<string name="settings_dynamic_color_unavailable">Requires Android 12 or newer</string>
|
||||||
|
<string name="settings_font_headings">Headings font</string>
|
||||||
|
<string name="settings_font_body">Body font</string>
|
||||||
|
<string name="settings_font_system">System default</string>
|
||||||
|
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
|
||||||
|
<string name="font_lora">Lora</string>
|
||||||
|
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||||
|
<string name="settings_font_choose_file">Choose file…</string>
|
||||||
|
<string name="settings_font_custom_selected">Custom font</string>
|
||||||
|
<string name="settings_font_import_failed">Couldn\'t read that file as a font</string>
|
||||||
<string name="settings_week_start">Week starts on</string>
|
<string name="settings_week_start">Week starts on</string>
|
||||||
<string name="settings_week_start_auto">Automatic</string>
|
<string name="settings_week_start_auto">Automatic</string>
|
||||||
<string name="settings_time_format">Time format</string>
|
<string name="settings_time_format">Time format</string>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.theme
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class FontsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromToken round-trips every bundled font`() {
|
||||||
|
BundledFont.entries.forEach { font ->
|
||||||
|
assertThat(BundledFont.fromToken(font.token)).isEqualTo(font)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fromToken is null for system, custom, and unknown tokens`() {
|
||||||
|
assertThat(BundledFont.fromToken(FONT_SYSTEM_TOKEN)).isNull()
|
||||||
|
assertThat(BundledFont.fromToken(FONT_CUSTOM_TOKEN)).isNull()
|
||||||
|
assertThat(BundledFont.fromToken("not-a-font")).isNull()
|
||||||
|
assertThat(BundledFont.fromToken(null)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `system for both roles returns the untouched default typography`() {
|
||||||
|
assertThat(calendulaTypography(brand = null, plain = null))
|
||||||
|
.isSameInstanceAs(CalendulaTypography)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `brand drives display and headline, plain drives title, body, and label`() {
|
||||||
|
val brand = BundledFont.Lora.family
|
||||||
|
val plain = BundledFont.JetBrainsMono.family
|
||||||
|
val typography = calendulaTypography(brand = brand, plain = plain)
|
||||||
|
|
||||||
|
assertThat(typography.displayLarge.fontFamily).isEqualTo(brand)
|
||||||
|
assertThat(typography.headlineSmall.fontFamily).isEqualTo(brand)
|
||||||
|
assertThat(typography.titleLarge.fontFamily).isEqualTo(plain)
|
||||||
|
assertThat(typography.bodyMedium.fontFamily).isEqualTo(plain)
|
||||||
|
assertThat(typography.labelSmall.fontFamily).isEqualTo(plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a null role keeps that role's default family while the other is applied`() {
|
||||||
|
val plain = BundledFont.Lora.family
|
||||||
|
val typography = calendulaTypography(brand = null, plain = plain)
|
||||||
|
|
||||||
|
// Brand styles keep the Material default; plain styles switch.
|
||||||
|
assertThat(typography.displayLarge.fontFamily)
|
||||||
|
.isEqualTo(CalendulaTypography.displayLarge.fontFamily)
|
||||||
|
assertThat(typography.bodyLarge.fontFamily).isEqualTo(plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
92
licenses/fonts/AtkinsonHyperlegible-OFL.txt
Normal file
92
licenses/fonts/AtkinsonHyperlegible-OFL.txt
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
Copyright 2020 Braille Institute of America, Inc.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
licenses/fonts/JetBrainsMono-OFL.txt
Normal file
93
licenses/fonts/JetBrainsMono-OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
licenses/fonts/Lora-OFL.txt
Normal file
93
licenses/fonts/Lora-OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
17
licenses/fonts/README.md
Normal file
17
licenses/fonts/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Bundled fonts
|
||||||
|
|
||||||
|
Calendula bundles the following fonts as optional user-selectable typefaces
|
||||||
|
(Settings → Appearance → Fonts). Each is licensed under the SIL Open Font
|
||||||
|
License 1.1; the full license text is included alongside this file.
|
||||||
|
|
||||||
|
| Font | Files (`app/src/main/res/font/`) | Copyright | License |
|
||||||
|
|------|----------------------------------|-----------|---------|
|
||||||
|
| Atkinson Hyperlegible | `atkinson_hyperlegible_regular.ttf`, `atkinson_hyperlegible_bold.ttf` | © 2020 Braille Institute of America, Inc. | [OFL](AtkinsonHyperlegible-OFL.txt) |
|
||||||
|
| Lora (variable) | `lora.ttf` | © 2011 Cyreal | [OFL](Lora-OFL.txt) |
|
||||||
|
| JetBrains Mono | `jetbrains_mono_regular.ttf`, `jetbrains_mono_bold.ttf` | © 2020 The JetBrains Mono Project Authors | [OFL](JetBrainsMono-OFL.txt) |
|
||||||
|
|
||||||
|
The device's default typeface (Roboto on most Android builds) remains the
|
||||||
|
default and is not bundled.
|
||||||
|
|
||||||
|
Fonts a user loads from their own device via "Choose file…" are copied into the
|
||||||
|
app's private storage and are not redistributed.
|
||||||
Reference in New Issue
Block a user