Merge pull request 'feat(theme): user-selectable custom fonts (#19)' (!55) from feat/custom-fonts into release/v2.13.0
Reviewed-on: #55
This commit was merged in pull request #55.
This commit is contained in:
@@ -34,8 +34,11 @@ import de.jeanlucmakiola.calendula.ui.common.DebugRibbon
|
||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity
|
||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
||||
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.theme.CalendulaTheme
|
||||
import de.jeanlucmakiola.calendula.ui.theme.calendulaTypography
|
||||
import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
@@ -99,9 +102,20 @@ class MainActivity : AppCompatActivity() {
|
||||
val use24Hour = remember(settings.timeFormat, 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(
|
||||
darkTheme = darkTheme,
|
||||
dynamicColor = settings.dynamicColor,
|
||||
typography = typography,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
CompositionLocalProvider(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
|
||||
import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
@@ -106,6 +107,27 @@ class SettingsPrefs @Inject constructor(
|
||||
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) {
|
||||
store.edit { it[WEEK_START_KEY] = pref.storageValue() }
|
||||
}
|
||||
@@ -501,6 +523,8 @@ class SettingsPrefs @Inject constructor(
|
||||
companion object {
|
||||
internal val THEME_MODE_KEY = stringPreferencesKey("theme_mode")
|
||||
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 AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_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.pm.PackageManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.text.format.DateFormat
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -53,6 +55,7 @@ import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -62,6 +65,7 @@ import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -76,6 +80,7 @@ import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -97,9 +102,11 @@ import de.jeanlucmakiola.calendula.qs.NewEventTileService
|
||||
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
|
||||
import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker
|
||||
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.calendarExpandEnter
|
||||
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.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
||||
@@ -122,6 +129,10 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
|
||||
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.LocalTime
|
||||
import java.time.format.TextStyle as JavaTextStyle
|
||||
@@ -498,6 +509,18 @@ private fun AppearanceScreen(
|
||||
var showAgendaScreenRange by remember { mutableStateOf(false) }
|
||||
var showAgendaWidgetRange 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(
|
||||
title = stringResource(R.string.settings_section_appearance),
|
||||
@@ -534,6 +557,24 @@ private fun AppearanceScreen(
|
||||
|
||||
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
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_default_view),
|
||||
@@ -625,6 +666,26 @@ private fun AppearanceScreen(
|
||||
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) {
|
||||
OptionPicker(
|
||||
title = stringResource(R.string.settings_week_start),
|
||||
@@ -1332,6 +1393,145 @@ 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* MIME types offered to the document picker so it lists only font files. Covers
|
||||
* the modern `font/` types plus the legacy `application/` font aliases some
|
||||
* providers still report. Anything that slips through is still validated by
|
||||
* [de.jeanlucmakiola.calendula.data.fonts.CustomFontStore] before use.
|
||||
*/
|
||||
private val FONT_PICKER_MIME_TYPES = arrayOf(
|
||||
"font/ttf",
|
||||
"font/otf",
|
||||
"font/sfnt",
|
||||
"font/collection",
|
||||
"application/x-font-ttf",
|
||||
"application/x-font-otf",
|
||||
"application/font-sfnt",
|
||||
"application/vnd.ms-opentype",
|
||||
)
|
||||
|
||||
/**
|
||||
* 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(FONT_PICKER_MIME_TYPES) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
private val WEEK_START_OPTIONS: List<WeekStartPref> =
|
||||
listOf(WeekStartPref.Auto) + DayOfWeek.entries.map { WeekStartPref.Day(it) }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.jeanlucmakiola.calendula.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
@@ -10,6 +11,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
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.PastEventDisplay
|
||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||
@@ -18,15 +21,21 @@ import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
||||
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
|
||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||
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.storageValue
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
|
||||
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_RANGE_KEY
|
||||
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
|
||||
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
@@ -36,12 +45,14 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val prefs: SettingsPrefs,
|
||||
repository: CalendarRepository,
|
||||
@IoDispatcher private val io: CoroutineDispatcher,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -140,6 +151,27 @@ class SettingsViewModel @Inject constructor(
|
||||
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
|
||||
// updateAll calls at once — concurrent calls coalesce around a stale read
|
||||
// and can leave the widget on the intermediate value. Held sequentially,
|
||||
@@ -189,6 +221,31 @@ class SettingsViewModel @Inject constructor(
|
||||
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) {
|
||||
viewModelScope.launch {
|
||||
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.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.MotionScheme
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -15,15 +16,18 @@ import androidx.compose.ui.platform.LocalContext
|
||||
* - System light/dark.
|
||||
* - Dynamic Color on API 31+, else falls back to the hand-tuned scheme
|
||||
* 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,
|
||||
* but the V1 foundation just follows the system.
|
||||
* The Settings screen overrides darkTheme, dynamicColor, and typography; the
|
||||
* bare defaults (used by the crash screen) just follow the system.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun CalendulaTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
typography: Typography = CalendulaTypography,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = when {
|
||||
@@ -42,7 +46,7 @@ fun CalendulaTheme(
|
||||
// the bouncy variant felt overdone in review (2026-06-11).
|
||||
MaterialExpressiveTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = CalendulaTypography,
|
||||
typography = typography,
|
||||
motionScheme = MotionScheme.standard(),
|
||||
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_dynamic_color">Dynamische Farben</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_auto">Automatisch</string>
|
||||
<string name="settings_time_format">Zeitformat</string>
|
||||
|
||||
@@ -274,6 +274,15 @@
|
||||
<string name="settings_default_view">Default view</string>
|
||||
<string name="settings_dynamic_color">Dynamic colour</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_auto">Automatic</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