feat: bundle Luciole, Literata, Charis SIL and a Hebrew fallback face
This commit is contained in:
parent
dfba1cfb16
commit
6de6672ae3
30 changed files with 624 additions and 11 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:signals/signals.dart';
|
||||
|
||||
import '../../database/database.dart';
|
||||
import '../fonts.dart';
|
||||
|
||||
/// The reader's presentation choices (spec 8.2) plus the per-app limits they
|
||||
/// move within. Values persist through ReaderSettings in the database.
|
||||
|
|
@ -19,7 +20,7 @@ class ReaderSettingsController {
|
|||
static const themeModes = ['light', 'dark', 'system'];
|
||||
static const variants = ['site', 'warm', 'cool', 'green', 'mono'];
|
||||
|
||||
final font = signal('Atkinson Hyperlegible');
|
||||
final font = signal(defaultFontFamily);
|
||||
final fontScale = signal(1.0);
|
||||
final themeMode = signal('system');
|
||||
final variant = signal('site');
|
||||
|
|
@ -34,7 +35,11 @@ class ReaderSettingsController {
|
|||
}
|
||||
|
||||
Future<bool> setFont(String family) async {
|
||||
if (family.isEmpty || family == font.value) return false;
|
||||
// Only a family this build bundles: an unknown one would silently render
|
||||
// in the platform's font instead.
|
||||
if (!readerFonts.any((f) => f.family == family) || family == font.value) {
|
||||
return false;
|
||||
}
|
||||
font.value = family;
|
||||
await _persist();
|
||||
return true;
|
||||
|
|
|
|||
60
app/lib/application/fonts.dart
Normal file
60
app/lib/application/fonts.dart
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/// The typefaces bundled with the client (spec 8.2: the reader chooses).
|
||||
/// Pure data, shared by the settings page that offers them and the renderer
|
||||
/// that applies them.
|
||||
library;
|
||||
|
||||
/// One bundled family: the name used in pubspec.yaml and stored in settings,
|
||||
/// the id of its display name in the locale file, and where to look when it
|
||||
/// has no glyph for a character.
|
||||
typedef ReaderFont = ({String family, String labelId, List<String> fallback});
|
||||
|
||||
/// Offered in this order: the two sans faces first, then the two serifs.
|
||||
/// Adding a font is four files in assets/fonts, one `fonts` entry in
|
||||
/// pubspec.yaml, one entry here, one label in the locale file, and its licence
|
||||
/// beside the files.
|
||||
///
|
||||
/// Each face names where to look for a character it has no glyph for. Hebrew
|
||||
/// comes from a bundled Hebrew face, matched to the shape of the chosen one,
|
||||
/// so it never depends on what the device happens to have installed. The
|
||||
/// serifs also cover Greek, Cyrillic and a wide Latin range, so each falls
|
||||
/// back to the other; the sans faces are narrow and have no bundled sans to
|
||||
/// turn to, so for those the platform answers, and it answers a sans with a
|
||||
/// sans. Scripts nothing here covers, such as Arabic and the CJK languages,
|
||||
/// always come from the platform.
|
||||
const readerFonts = <ReaderFont>[
|
||||
(
|
||||
family: 'Atkinson Hyperlegible',
|
||||
labelId: 'settings-font-atkinson',
|
||||
fallback: [hebrewSans],
|
||||
),
|
||||
(
|
||||
family: 'Luciole',
|
||||
labelId: 'settings-font-luciole',
|
||||
fallback: [hebrewSans],
|
||||
),
|
||||
(
|
||||
family: 'Literata',
|
||||
labelId: 'settings-font-literata',
|
||||
fallback: ['Charis SIL', hebrewSerif],
|
||||
),
|
||||
(
|
||||
family: 'Charis SIL',
|
||||
labelId: 'settings-font-charis',
|
||||
fallback: ['Literata', hebrewSerif],
|
||||
),
|
||||
];
|
||||
|
||||
/// Bundled to be fallen back on, never offered: the reader picks a face for
|
||||
/// the Latin text they are reading, and Hebrew follows its shape.
|
||||
const hebrewSans = 'Noto Sans Hebrew';
|
||||
const hebrewSerif = 'Noto Serif Hebrew';
|
||||
|
||||
const defaultFontFamily = 'Atkinson Hyperlegible';
|
||||
|
||||
/// Where to look after [family], or nothing when it is not one of ours.
|
||||
List<String> fallbackFor(String family) {
|
||||
for (final font in readerFonts) {
|
||||
if (font.family == family) return font.fallback;
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
|
@ -1,17 +1,11 @@
|
|||
import '../../l10n/strings.dart';
|
||||
import '../../mews/document.dart';
|
||||
import '../fonts.dart';
|
||||
import '../controllers/reader_settings_controller.dart'
|
||||
show ReaderSettingsController;
|
||||
import 'mews_urls.dart';
|
||||
import 'page_blocks.dart';
|
||||
|
||||
/// The bundled typefaces the reader can pick. Adding a font is one TTF in
|
||||
/// assets/fonts, one entry in pubspec.yaml, one entry here and one label in
|
||||
/// the locale file.
|
||||
const _fonts = <({String family, String labelId})>[
|
||||
(family: 'Atkinson Hyperlegible', labelId: 'settings-font-atkinson'),
|
||||
];
|
||||
|
||||
/// Message ids for the values the controller accepts. Kept literal so every
|
||||
/// id in the locale file can be found by searching for it.
|
||||
const _themeLabels = {
|
||||
|
|
@ -43,7 +37,7 @@ Future<MewsPage> buildSettingsPage(
|
|||
...chrome(title: title, current: url, strings: strings),
|
||||
HeadingBlock(2, [MewsText(strings.t('settings-font-heading'))]),
|
||||
choiceRow([
|
||||
for (final font in _fonts)
|
||||
for (final font in readerFonts)
|
||||
(
|
||||
href: MewsUrls.action('set-font', {'value': font.family}),
|
||||
label: strings.t(font.labelId),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import '../application/controllers/bookmarks_controller.dart';
|
|||
import '../application/controllers/browser_controller.dart';
|
||||
import '../application/controllers/reader_settings_controller.dart';
|
||||
import '../application/controllers/subscriptions_controller.dart';
|
||||
import '../application/fonts.dart';
|
||||
import '../application/internal_actions.dart';
|
||||
import '../application/internal_pages.dart';
|
||||
import '../database/database.dart';
|
||||
|
|
@ -171,6 +172,7 @@ class _ThemedApp extends StatelessWidget {
|
|||
// The chrome reads in the typeface the reader chose for pages, so the
|
||||
// address bar and the page below it are not two different faces.
|
||||
fontFamily: fontFamily,
|
||||
fontFamilyFallback: fallbackFor(fontFamily),
|
||||
scaffoldBackgroundColor: palette.background,
|
||||
// Selection highlights and the caret follow the palette too.
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../l10n/strings.dart';
|
||||
import '../../application/fonts.dart';
|
||||
import '../../mews/document.dart';
|
||||
import 'palettes.dart';
|
||||
|
||||
|
|
@ -117,6 +118,7 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
style: TextStyle(
|
||||
color: palette.foreground.withValues(alpha: 0.7),
|
||||
fontFamily: widget.fontFamily,
|
||||
fontFamilyFallback: fallbackFor(widget.fontFamily),
|
||||
fontSize: base * 0.85,
|
||||
),
|
||||
),
|
||||
|
|
@ -129,6 +131,7 @@ class _MewsPageViewState extends State<MewsPageView> {
|
|||
final foreground = TextStyle(
|
||||
color: palette.foreground,
|
||||
fontFamily: widget.fontFamily,
|
||||
fontFamilyFallback: fallbackFor(widget.fontFamily),
|
||||
fontSize: base,
|
||||
height: 1.5,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue