feat(app): Noto Sans and Noto Sans JP, bundled, the same on every device
The app brought no fonts: text took each device's own, a minimal Linux showed Japanese as boxes, and kanji could take Chinese shapes. Now Noto Sans is the app's font everywhere (Latin, Greek with polytonic, Cyrillic), and Noto Sans JP fills in kana and kanji in their Japanese shapes. Noto Sans JP alone wouldn't do: it lacks accented and polytonic Greek. Both come with the app and are never fetched; scripts neither covers fall back to the device's own fonts. Each family is one variable font holding every weight (Flutter sets the weight axis from the requested weight; a test checks bold inks more): 0.7 MB Noto Sans, 0.7 MB its italic, 8.1 MB Noto Sans JP, from Noto's own GitHub releases (assets/fonts/ATTRIBUTION.md). Their SIL Open Font License texts join the app's licence list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b4fe6814ef
commit
4345741646
12 changed files with 331 additions and 21 deletions
|
|
@ -97,7 +97,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
- **Material comes from `package:material_ui`** (Cupertino from `package:cupertino_ui`), never from `package:flutter/material.dart`, which Flutter deprecates from its November 2026 release on. Packages still on the SDK's copy get the app's theme and localizations through `MaterialUiCompatibilityBridge` in `MaterialApp.builder`; remove it once they have moved.
|
- **Material comes from `package:material_ui`** (Cupertino from `package:cupertino_ui`), never from `package:flutter/material.dart`, which Flutter deprecates from its November 2026 release on. Packages still on the SDK's copy get the app's theme and localizations through `MaterialUiCompatibilityBridge` in `MaterialApp.builder`; remove it once they have moved.
|
||||||
- **Navigation is `go_router`**: the routes in `lib/src/app/app_router.dart`, their paths in `core/router/route_paths.dart`. Four tabs (Home, Library, Explore, Profile) are the branches of a `StatefulShellRoute`, each keeping its own stack of screens alive while another tab shows (`tabsContainer`, go_router's indexed stack plus `TabVisibility`, which a screen depends on to stop something once its tab is hidden); `AppShell` shows them as a navigation rail from 600 dp on and as a navigation bar below, and choosing the open tab again returns to its first screen. A tab's own screens are routes nested in its branch, reached with `context.go` (a studyset's screen in the library), and a studyset's topic opens on top of it with `context.push` (the same route, keyed by id, so several can stack); the breadcrumb's steps go up with `context.go`, so back then leads further up. A studyset's notes and learning paths (working through one, writing it, writing a step) are routes of the Library branch too, so the tabs stay beside them. What needs the whole window (studying, a card, a PDF, settings) is a top-level route over the tabs, reached with `context.push`. Explore stays empty until something is shared.
|
- **Navigation is `go_router`**: the routes in `lib/src/app/app_router.dart`, their paths in `core/router/route_paths.dart`. Four tabs (Home, Library, Explore, Profile) are the branches of a `StatefulShellRoute`, each keeping its own stack of screens alive while another tab shows (`tabsContainer`, go_router's indexed stack plus `TabVisibility`, which a screen depends on to stop something once its tab is hidden); `AppShell` shows them as a navigation rail from 600 dp on and as a navigation bar below, and choosing the open tab again returns to its first screen. A tab's own screens are routes nested in its branch, reached with `context.go` (a studyset's screen in the library), and a studyset's topic opens on top of it with `context.push` (the same route, keyed by id, so several can stack); the breadcrumb's steps go up with `context.go`, so back then leads further up. A studyset's notes and learning paths (working through one, writing it, writing a step) are routes of the Library branch too, so the tabs stay beside them. What needs the whole window (studying, a card, a PDF, settings) is a top-level route over the tabs, reached with `context.push`. Explore stays empty until something is shared.
|
||||||
- **Routes carry ids, never objects.** A screen finds its item in the repository's live list with `FoundById` (`core/widget/`): a spinner until the list arrives, then the item, or a screen saying it no longer exists (deleted on another device, or an old link). A screen that deletes its item closes, and keeps showing the item while it goes. Why: every location can be built from its path alone, and no screen goes on showing something that has gone.
|
- **Routes carry ids, never objects.** A screen finds its item in the repository's live list with `FoundById` (`core/widget/`): a spinner until the list arrives, then the item, or a screen saying it no longer exists (deleted on another device, or an old link). A screen that deletes its item closes, and keeps showing the item while it goes. Why: every location can be built from its path alone, and no screen goes on showing something that has gone.
|
||||||
- **The look is one theme** (`core/theme/`): `appTheme(brightness)` with hand-written light and dark colour schemes around the brand's cobalt and copper; `Theme.of(context).extras` (`ThemeExtras`) for the colours Material has no role for (states, the four FSRS ratings, a starred star); `PaletteColor` for the 20 colours a person can give content. `theme_test.dart` holds text to WCAG AA contrast (4.5:1) on each palette colour and on the dark theme's primary colour and surface. Layouts follow Material 3's window-size classes (`context.windowSize`, `core/layout/`), and running text stays within `readableContentMaxWidth`. Icons are Lucide (`lucide_icons_flutter`), except a starred star and the marks of a choice not yet checked (filled box and circle), which need the fill Lucide lacks; the Material icons left in older screens go when those are restyled. Shared widgets are in `core/widget/` (`SectionCard`, `AppLoadingIndicator`, `confirmDestructiveAction`, `FoundById`, `TitleWithKicker`, `ContentCountsLine`, `showUndoSnackBar`, `ScrollHidingFab`, `PaletteColorPicker`, `CountStepper`, `RequiredFieldLabel` and `FieldInfoButton`, `ReorderToggle`, `ReorderGrip`, `liftWhileDragged`, `SliverCentered` and `EmptyListMessage` (what an empty list shows: icon, "No … yet", and which menus add some), and the sheets below).
|
- **The look is one theme** (`core/theme/`): `appTheme(brightness)` with hand-written light and dark colour schemes around the brand's cobalt and copper; `Theme.of(context).extras` (`ThemeExtras`) for the colours Material has no role for (states, the four FSRS ratings, a starred star); `PaletteColor` for the 20 colours a person can give content. `theme_test.dart` holds text to WCAG AA contrast (4.5:1) on each palette colour and on the dark theme's primary colour and surface. **Fonts are bundled, never fetched** (`core/theme/app_fonts.dart`, `assets/fonts/ATTRIBUTION.md`): Noto Sans is the app's font on every device (Latin, Greek with polytonic, Cyrillic), Noto Sans JP fills in kana and kanji in their Japanese shapes; each is one variable font holding every weight, and their OFL licences join the licence list. Scripts neither covers (Chinese, Korean, Arabic, …) fall back to the device's own fonts. Never add `google_fonts` or anything else that downloads fonts. Layouts follow Material 3's window-size classes (`context.windowSize`, `core/layout/`), and running text stays within `readableContentMaxWidth`. Icons are Lucide (`lucide_icons_flutter`), except a starred star and the marks of a choice not yet checked (filled box and circle), which need the fill Lucide lacks; the Material icons left in older screens go when those are restyled. Shared widgets are in `core/widget/` (`SectionCard`, `AppLoadingIndicator`, `confirmDestructiveAction`, `FoundById`, `TitleWithKicker`, `ContentCountsLine`, `showUndoSnackBar`, `ScrollHidingFab`, `PaletteColorPicker`, `CountStepper`, `RequiredFieldLabel` and `FieldInfoButton`, `ReorderToggle`, `ReorderGrip`, `liftWhileDragged`, `SliverCentered` and `EmptyListMessage` (what an empty list shows: icon, "No … yet", and which menus add some), and the sheets below).
|
||||||
- **Lists that can grow long are built as they scroll into view**: a studyset's tabs are `CustomScrollView`s holding each list's slivers (`SliverList`, `SliverReorderableList`), and what a list says in place of its rows sits in a `SliverCentered`. Never a `shrinkWrap` list inside another scrolling view: that builds every row at once, and 1,000 cards then froze the window for seconds, long enough for the desktop to offer to kill the app. Rows are put in another order in a Reorder mode (`ReorderToggle` beside the list; a `ReorderGrip` in place of each row's options; positions from `core/sync/fractional_position.dart`), never with Flutter's default drag handles, which on the desktop lie over a row's trailing end, its … button. Flashcards have no Reorder (decided 2026-10-08): they keep the order they were created or imported in, which new cards and cramming follow, and sub-studysets put lessons in order; no one drags a card among a thousand.
|
- **Lists that can grow long are built as they scroll into view**: a studyset's tabs are `CustomScrollView`s holding each list's slivers (`SliverList`, `SliverReorderableList`), and what a list says in place of its rows sits in a `SliverCentered`. Never a `shrinkWrap` list inside another scrolling view: that builds every row at once, and 1,000 cards then froze the window for seconds, long enough for the desktop to offer to kill the app. Rows are put in another order in a Reorder mode (`ReorderToggle` beside the list; a `ReorderGrip` in place of each row's options; positions from `core/sync/fractional_position.dart`), never with Flutter's default drag handles, which on the desktop lie over a row's trailing end, its … button. Flashcards have no Reorder (decided 2026-10-08): they keep the order they were created or imported in, which new cards and cramming follow, and sub-studysets put lessons in order; no one drags a card among a thousand.
|
||||||
- **Forms and pickers open as sheets**: `showAdaptiveSheet` with a `SheetScaffold` (title and close button on top, the body scrolling with a scrollbar, always shown on the desktop, and Material 3's lines under the title and above the footer while some of the body lies beyond them, the action pinned below) shows a bottom sheet below expanded windows and a dialog from there on. A sheet that asks before discarding changes (a `PopScope`) passes `swipeable: false`: a bottom sheet swiped away closes without asking its `PopScope` (Flutter issue 152120, open with v3.47). A sheet offering an Undo passes `hasSnackBars: true`, so its snack bars show on it rather than behind its barrier. A picker that changes something outside it reports every change as it happens (`onChanged`) rather than as its result, since a sheet swiped away has none.
|
- **Forms and pickers open as sheets**: `showAdaptiveSheet` with a `SheetScaffold` (title and close button on top, the body scrolling with a scrollbar, always shown on the desktop, and Material 3's lines under the title and above the footer while some of the body lies beyond them, the action pinned below) shows a bottom sheet below expanded windows and a dialog from there on. A sheet that asks before discarding changes (a `PopScope`) passes `swipeable: false`: a bottom sheet swiped away closes without asking its `PopScope` (Flutter issue 152120, open with v3.47). A sheet offering an Undo passes `hasSnackBars: true`, so its snack bars show on it rather than behind its barrier. A picker that changes something outside it reports every change as it happens (`onChanged`) rather than as its result, since a sheet swiped away has none.
|
||||||
- **Text comes from `AppLocalizations`** (Flutter's gen-l10n, `lib/l10n/`): no string a person reads is written into a widget. The app speaks British English, American English and German (informal "du"; the terms of `app_de.arb`, such as Lernset and Karteikarte, stay consistent). `app_en.arb` is the template and holds British English, also the fallback for any language the app doesn't speak; `app_en_US.arb` overrides only what American English spells differently, and `app_en_GB.arb` is empty, there so that Material and dates format the British way. A new message goes into `app_en.arb` and `app_de.arb` together (`test/l10n/arb_files_test.dart` fails otherwise, and checks the placeholders), and into `app_en_US.arb` if its spelling differs. `resolveAppLocale` (`core/l10n/`) picks the language, never by country alone, and both `MaterialApp`s take `appLocalizationsDelegates`, not gen-l10n's list: that brings flutter_localizations' Material texts, which serve only the SDK's Material, so material_ui's widgets would stay English. Errors found in data go up as values the widgets put into words (`PathImportProblem`, `PathImportTexts`), never as English sentences. The editor's own placeholders are replaced by ours (`buildersWithUnknown`), since the editor's texts follow a process-wide locale of their own. The generated `lib/l10n/app_localizations*.dart` are committed and never edited, analysed or formatted by hand. `flutter pub get` writes them anew, so CI fails when they don't match the `.arb` files.
|
- **Text comes from `AppLocalizations`** (Flutter's gen-l10n, `lib/l10n/`): no string a person reads is written into a widget. The app speaks British English, American English and German (informal "du"; the terms of `app_de.arb`, such as Lernset and Karteikarte, stay consistent). `app_en.arb` is the template and holds British English, also the fallback for any language the app doesn't speak; `app_en_US.arb` overrides only what American English spells differently, and `app_en_GB.arb` is empty, there so that Material and dates format the British way. A new message goes into `app_en.arb` and `app_de.arb` together (`test/l10n/arb_files_test.dart` fails otherwise, and checks the placeholders), and into `app_en_US.arb` if its spelling differs. `resolveAppLocale` (`core/l10n/`) picks the language, never by country alone, and both `MaterialApp`s take `appLocalizationsDelegates`, not gen-l10n's list: that brings flutter_localizations' Material texts, which serve only the SDK's Material, so material_ui's widgets would stay English. Errors found in data go up as values the widgets put into words (`PathImportProblem`, `PathImportTexts`), never as English sentences. The editor's own placeholders are replaced by ours (`buildersWithUnknown`), since the editor's texts follow a process-wide locale of their own. The generated `lib/l10n/app_localizations*.dart` are committed and never edited, analysed or formatted by hand. `flutter pub get` writes them anew, so CI fails when they don't match the `.arb` files.
|
||||||
|
|
|
||||||
14
halkyon_learn_flutter/assets/fonts/ATTRIBUTION.md
Normal file
14
halkyon_learn_flutter/assets/fonts/ATTRIBUTION.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Fonts
|
||||||
|
|
||||||
|
The app brings its fonts with it and never fetches any: text looks the same on
|
||||||
|
every device, whatever fonts it has.
|
||||||
|
|
||||||
|
| File | Font | Source | Licence |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `NotoSans-Variable.ttf`, `NotoSans-Italic-Variable.ttf` | Noto Sans (Latin, Greek incl. polytonic, Cyrillic), every weight | [notofonts/latin-greek-cyrillic](https://github.com/notofonts/latin-greek-cyrillic) release NotoSans-v2.015, `unhinted/slim-variable-ttf` | SIL Open Font License 1.1, `OFL-NotoSans.txt` |
|
||||||
|
| `NotoSansJP-Variable.otf` | Noto Sans JP (kana, kanji in Japanese shapes), every weight | [notofonts/noto-cjk](https://github.com/notofonts/noto-cjk) `Sans/Variable/OTF/Subset/NotoSansJP-VF.otf`, commit f8d157532fbf (2026-10-08) | SIL Open Font License 1.1, `OFL-NotoSansJP.txt` |
|
||||||
|
|
||||||
|
Noto Sans is the app's font; Noto Sans JP fills in the glyphs Noto Sans lacks
|
||||||
|
(`lib/src/core/theme/app_fonts.dart`). Noto Sans JP alone would not do: it has
|
||||||
|
no accented or polytonic Greek. The files are renamed only; their glyphs are
|
||||||
|
unchanged. The licences are shown in the app's licence list.
|
||||||
BIN
halkyon_learn_flutter/assets/fonts/NotoSans-Italic-Variable.ttf
Normal file
BIN
halkyon_learn_flutter/assets/fonts/NotoSans-Italic-Variable.ttf
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/fonts/NotoSans-Variable.ttf
Normal file
BIN
halkyon_learn_flutter/assets/fonts/NotoSans-Variable.ttf
Normal file
Binary file not shown.
BIN
halkyon_learn_flutter/assets/fonts/NotoSansJP-Variable.otf
Normal file
BIN
halkyon_learn_flutter/assets/fonts/NotoSansJP-Variable.otf
Normal file
Binary file not shown.
93
halkyon_learn_flutter/assets/fonts/OFL-NotoSans.txt
Normal file
93
halkyon_learn_flutter/assets/fonts/OFL-NotoSans.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
|
||||||
|
|
||||||
|
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.
|
||||||
92
halkyon_learn_flutter/assets/fonts/OFL-NotoSansJP.txt
Normal file
92
halkyon_learn_flutter/assets/fonts/OFL-NotoSansJP.txt
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
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.
|
||||||
|
|
@ -3,12 +3,14 @@ import 'package:halkyon_learn_flutter/src/app/person_app.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/bloc/app_bloc_observer.dart';
|
import 'package:halkyon_learn_flutter/src/core/bloc/app_bloc_observer.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/theme/app_fonts.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/auth/widget/session_gate.dart';
|
import 'package:halkyon_learn_flutter/src/feature/auth/widget/session_gate.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
Bloc.observer = const AppBlocObserver();
|
Bloc.observer = const AppBlocObserver();
|
||||||
|
registerAppFontLicenses();
|
||||||
final dependencies = await Dependencies.create();
|
final dependencies = await Dependencies.create();
|
||||||
runApp(
|
runApp(
|
||||||
DependenciesScope(
|
DependenciesScope(
|
||||||
|
|
|
||||||
25
halkyon_learn_flutter/lib/src/core/theme/app_fonts.dart
Normal file
25
halkyon_learn_flutter/lib/src/core/theme/app_fonts.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// The app's font, the same on every device: Noto Sans, for Latin, Greek
|
||||||
|
/// (polytonic too) and Cyrillic. Bundled, never fetched (pubspec.yaml), so
|
||||||
|
/// text looks alike everywhere and needs nothing the device may lack.
|
||||||
|
const appFontFamily = 'Noto Sans';
|
||||||
|
|
||||||
|
/// What fills in where [appFontFamily] has no glyph: Noto Sans JP, for kana
|
||||||
|
/// and kanji in their Japanese shapes. Noto Sans JP alone can't be the app's
|
||||||
|
/// font: it lacks accented and polytonic Greek.
|
||||||
|
const appFontFallback = ['Noto Sans JP'];
|
||||||
|
|
||||||
|
/// Adds the bundled fonts' licences to the app's licence list, beside the
|
||||||
|
/// packages' own: the SIL Open Font License asks that it travels with them.
|
||||||
|
void registerAppFontLicenses() => LicenseRegistry.addLicense(() async* {
|
||||||
|
for (final (family, file) in const [
|
||||||
|
('Noto Sans', 'assets/fonts/OFL-NotoSans.txt'),
|
||||||
|
('Noto Sans JP', 'assets/fonts/OFL-NotoSansJP.txt'),
|
||||||
|
]) {
|
||||||
|
yield LicenseEntryWithLineBreaks([
|
||||||
|
family,
|
||||||
|
], await rootBundle.loadString(file));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/theme/app_fonts.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/theme/brand.dart';
|
import 'package:halkyon_learn_flutter/src/core/theme/brand.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/core/theme/theme_extras.dart';
|
import 'package:halkyon_learn_flutter/src/core/theme/theme_extras.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The app's theme for [brightness]: Material 3 with a hand-authored colour
|
/// The app's theme for [brightness]: Material 3 with a hand-authored colour
|
||||||
/// scheme, [ThemeExtras], and the current style of progress indicators.
|
/// scheme, the app's own fonts ([appFontFamily]), [ThemeExtras], and the
|
||||||
|
/// current style of progress indicators.
|
||||||
ThemeData appTheme(Brightness brightness) => ThemeData(
|
ThemeData appTheme(Brightness brightness) => ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
fontFamily: appFontFamily,
|
||||||
|
fontFamilyFallback: appFontFallback,
|
||||||
colorScheme: switch (brightness) {
|
colorScheme: switch (brightness) {
|
||||||
Brightness.light => _lightScheme(),
|
Brightness.light => _lightScheme(),
|
||||||
Brightness.dark => _darkScheme(),
|
Brightness.dark => _darkScheme(),
|
||||||
|
|
|
||||||
|
|
@ -147,26 +147,22 @@ flutter:
|
||||||
- assets/icon/halkyon_icon.png
|
- assets/icon/halkyon_icon.png
|
||||||
# Region maps for map questions (see assets/maps/ATTRIBUTION.md).
|
# Region maps for map questions (see assets/maps/ATTRIBUTION.md).
|
||||||
- assets/maps/japan_prefectures.geojson
|
- assets/maps/japan_prefectures.geojson
|
||||||
|
# The bundled fonts' licences, for the licence list (see assets/fonts/ATTRIBUTION.md).
|
||||||
|
- assets/fonts/OFL-NotoSans.txt
|
||||||
|
- assets/fonts/OFL-NotoSansJP.txt
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
# The app's own fonts, the same on every device and never fetched: Noto Sans for
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
# Latin, Greek and Cyrillic, Noto Sans JP behind it for kana and kanji
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
# (core/theme/app_fonts.dart). Each file holds every weight (variable fonts).
|
||||||
# list giving the asset and other descriptors for the font. For
|
fonts:
|
||||||
# example:
|
- family: Noto Sans
|
||||||
# fonts:
|
fonts:
|
||||||
# - family: Schyler
|
- asset: assets/fonts/NotoSans-Variable.ttf
|
||||||
# fonts:
|
- asset: assets/fonts/NotoSans-Italic-Variable.ttf
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
style: italic
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
- family: Noto Sans JP
|
||||||
# style: italic
|
fonts:
|
||||||
# - family: Trajan Pro
|
- asset: assets/fonts/NotoSansJP-Variable.otf
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|
||||||
# The app's icons, written by tool/app_icons.py: it prepares the images derived from
|
# The app's icons, written by tool/app_icons.py: it prepares the images derived from
|
||||||
# the kingfisher in build/icons/ first, then runs `dart run flutter_launcher_icons`
|
# the kingfisher in build/icons/ first, then runs `dart run flutter_launcher_icons`
|
||||||
|
|
|
||||||
84
halkyon_learn_flutter/test/core/theme/app_fonts_test.dart
Normal file
84
halkyon_learn_flutter/test/core/theme/app_fonts_test.dart
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/theme/app_fonts.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// Loads the bundled font [family] from [files], as the app does at start.
|
||||||
|
Future<void> load(String family, List<String> files) async {
|
||||||
|
final loader = FontLoader(family);
|
||||||
|
for (final file in files) {
|
||||||
|
loader.addFont(rootBundle.load(file));
|
||||||
|
}
|
||||||
|
await loader.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many pixels [text] inks in [family] at [weight]: heavier strokes ink
|
||||||
|
/// more.
|
||||||
|
Future<int> ink(String text, String family, FontWeight weight) async {
|
||||||
|
final painter = TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: family,
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: weight,
|
||||||
|
color: const Color(0xFF000000),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout();
|
||||||
|
final recorder = ui.PictureRecorder();
|
||||||
|
painter.paint(Canvas(recorder), Offset.zero);
|
||||||
|
final image = await recorder.endRecording().toImage(
|
||||||
|
painter.width.ceil(),
|
||||||
|
painter.height.ceil(),
|
||||||
|
);
|
||||||
|
final pixels = (await image.toByteData())!;
|
||||||
|
var inked = 0;
|
||||||
|
for (var alpha = 3; alpha < pixels.lengthInBytes; alpha += 4) {
|
||||||
|
if (pixels.getUint8(alpha) > 128) {
|
||||||
|
inked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('the theme sets text in Noto Sans, with Noto Sans JP for what it '
|
||||||
|
'lacks', () {
|
||||||
|
for (final brightness in Brightness.values) {
|
||||||
|
final style = appTheme(brightness).textTheme.bodyLarge!;
|
||||||
|
expect(style.fontFamily, 'Noto Sans');
|
||||||
|
expect(style.fontFamilyFallback, ['Noto Sans JP']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('each bundled font draws every weight from its one file', () async {
|
||||||
|
await load('Noto Sans', ['assets/fonts/NotoSans-Variable.ttf']);
|
||||||
|
await load('Noto Sans JP', ['assets/fonts/NotoSansJP-Variable.otf']);
|
||||||
|
|
||||||
|
for (final (family, text) in [
|
||||||
|
('Noto Sans', 'Ἀθῆναι Halkyon'),
|
||||||
|
('Noto Sans JP', '日本語のかな'),
|
||||||
|
]) {
|
||||||
|
final regular = await ink(text, family, FontWeight.w400);
|
||||||
|
final bold = await ink(text, family, FontWeight.w700);
|
||||||
|
expect(bold, greaterThan(regular * 1.2), reason: family);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the fonts' licences join the licence list", () async {
|
||||||
|
registerAppFontLicenses();
|
||||||
|
|
||||||
|
final packages = {
|
||||||
|
await for (final entry in LicenseRegistry.licenses) ...entry.packages,
|
||||||
|
};
|
||||||
|
expect(packages, containsAll(['Noto Sans', 'Noto Sans JP']));
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue