feat(appearance): support imported TTF and OTF fonts

This commit is contained in:
zarzet committed 2026-10-10 16:59:06 +07:00
1 parent cdceda4585
commit b30dd9fed0
12 files changed
+362 -7

No files matched your search

+4
View File
@@ -7152,6 +7152,10 @@
"playbackPauseOnMuteDescription": "Pause at zero media volume and resume when raised. Manual pauses stay paused. Standard audio output only.",
"playbackOnHeadphonesConnected": "Play when headphones connect",
"playbackOnHeadphonesConnectedDescription": "Resume the current song when wired or Bluetooth headphones connect. Disconnecting pauses playback. Built-in player only.",
"appearanceCustomFont": "Custom font",
"appearanceCustomFontSubtitle": "Import a TTF or OTF file (up to 10 MB). Overrides the theme and system font.",
"appearanceCustomFontInvalid": "Could not load this font. Choose a valid TTF or OTF file up to 10 MB.",
"appearanceCustomFontReset": "Use theme font",
"optionsEmbeddedCoverSizeOriginal": "Original resolution",
"@optionsEmbeddedCoverSizeOriginal": {
"description": "Option that preserves the provider artwork at its original resolution"
+4
View File
@@ -7165,6 +7165,10 @@
"artistUpcomingConcerts": "Konser Mendatang",
"artistAllUpcomingConcerts": "Semua Konser Mendatang",
"artistNoUpcomingConcerts": "Tiada konser mendatang.",
"appearanceCustomFont": "Font kustom",
"appearanceCustomFontSubtitle": "Impor file TTF atau OTF (maksimal 10 MB). Menggantikan font tema dan sistem.",
"appearanceCustomFontInvalid": "Font tidak dapat dimuat. Pilih file TTF atau OTF yang valid, maksimal 10 MB.",
"appearanceCustomFontReset": "Gunakan font tema",
"optionsEmbeddedCoverSizeOriginal": "Resolusi asli",
"@optionsEmbeddedCoverSizeOriginal": {
"description": "Option that preserves the provider artwork at its original resolution"
+18
View File
@@ -7,6 +7,8 @@ const String kUseAmoledKey = 'use_amoled';
const String kThemeStyleKey = 'theme_style';
const String kMornyeAccentKey = 'mornye_accent';
const String kUseSystemFontKey = 'use_system_font';
const String kCustomFontIdKey = 'custom_font_id';
const String kCustomFontNameKey = 'custom_font_name';
const String kMornyeGlassClarityKey = 'mornye_glass_clarity';
const String kMornyeGlassOutlinesKey = 'mornye_glass_outlines';
const double kDefaultMornyeGlassClarity = 0.75;
@@ -43,6 +45,8 @@ class ThemeSettings {
final AppThemeStyle style;
final MornyeAccent mornyeAccent;
final bool useSystemFont;
final String customFontId;
final String customFontName;
final double mornyeGlassClarity;
final bool mornyeGlassOutlines;
@@ -54,6 +58,8 @@ class ThemeSettings {
this.style = AppThemeStyle.material,
this.mornyeAccent = MornyeAccent.red,
this.useSystemFont = false,
this.customFontId = '',
this.customFontName = '',
this.mornyeGlassClarity = kDefaultMornyeGlassClarity,
this.mornyeGlassOutlines = true,
});
@@ -68,6 +74,8 @@ class ThemeSettings {
AppThemeStyle? style,
MornyeAccent? mornyeAccent,
bool? useSystemFont,
String? customFontId,
String? customFontName,
double? mornyeGlassClarity,
bool? mornyeGlassOutlines,
}) {
@@ -79,6 +87,8 @@ class ThemeSettings {
style: style ?? this.style,
mornyeAccent: mornyeAccent ?? this.mornyeAccent,
useSystemFont: useSystemFont ?? this.useSystemFont,
customFontId: customFontId ?? this.customFontId,
customFontName: customFontName ?? this.customFontName,
mornyeGlassClarity: normalizeMornyeGlassClarity(
mornyeGlassClarity ?? this.mornyeGlassClarity,
),
@@ -94,6 +104,8 @@ class ThemeSettings {
kThemeStyleKey: style.name,
kMornyeAccentKey: mornyeAccent.name,
kUseSystemFontKey: useSystemFont,
kCustomFontIdKey: customFontId,
kCustomFontNameKey: customFontName,
kMornyeGlassClarityKey: mornyeGlassClarity,
kMornyeGlassOutlinesKey: mornyeGlassOutlines,
};
@@ -107,6 +119,8 @@ class ThemeSettings {
style: themeStyleFromString(json[kThemeStyleKey] as String?),
mornyeAccent: mornyeAccentFromString(json[kMornyeAccentKey] as String?),
useSystemFont: json[kUseSystemFontKey] as bool? ?? false,
customFontId: json[kCustomFontIdKey] as String? ?? '',
customFontName: json[kCustomFontNameKey] as String? ?? '',
mornyeGlassClarity: normalizeMornyeGlassClarity(
json[kMornyeGlassClarityKey] as num?,
),
@@ -125,6 +139,8 @@ class ThemeSettings {
other.style == style &&
other.mornyeAccent == mornyeAccent &&
other.useSystemFont == useSystemFont &&
other.customFontId == customFontId &&
other.customFontName == customFontName &&
other.mornyeGlassClarity == mornyeGlassClarity &&
other.mornyeGlassOutlines == mornyeGlassOutlines;
}
@@ -138,6 +154,8 @@ class ThemeSettings {
style.hashCode ^
mornyeAccent.hashCode ^
useSystemFont.hashCode ^
customFontId.hashCode ^
customFontName.hashCode ^
mornyeGlassClarity.hashCode ^
mornyeGlassOutlines.hashCode;
}
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/providers/theme_provider.dart';
import 'package:spotiflac_android/services/custom_font_service.dart';
final customFontFamilyProvider = FutureProvider<String?>((ref) {
final id = ref.watch(themeProvider.select((theme) => theme.customFontId));
return CustomFontService.instance.restore(id);
});
+11
View File
@@ -16,6 +16,8 @@ ThemeSettings loadBootstrapThemeSettings(SharedPreferences prefs) {
style: themeStyleFromString(prefs.getString(kThemeStyleKey)),
mornyeAccent: mornyeAccentFromString(prefs.getString(kMornyeAccentKey)),
useSystemFont: prefs.getBool(kUseSystemFontKey) ?? false,
customFontId: prefs.getString(kCustomFontIdKey) ?? '',
customFontName: prefs.getString(kCustomFontNameKey) ?? '',
mornyeGlassClarity: normalizeMornyeGlassClarity(
prefs.getDouble(kMornyeGlassClarityKey),
),
@@ -77,6 +79,8 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
await prefs.setString(kThemeStyleKey, snapshot.style.name);
await prefs.setString(kMornyeAccentKey, snapshot.mornyeAccent.name);
await prefs.setBool(kUseSystemFontKey, snapshot.useSystemFont);
await prefs.setString(kCustomFontIdKey, snapshot.customFontId);
await prefs.setString(kCustomFontNameKey, snapshot.customFontName);
await prefs.setDouble(
kMornyeGlassClarityKey,
snapshot.mornyeGlassClarity,
@@ -125,6 +129,13 @@ class ThemeNotifier extends Notifier<ThemeSettings> {
await _saveToStorage();
}
Future<void> setCustomFont(String id, String name) async {
_editState(
(current) => current.copyWith(customFontId: id, customFontName: name),
);
await _saveToStorage();
}
Future<void> setMornyeGlassClarity(
double value, {
bool persist = true,
@@ -12,6 +12,7 @@ import 'package:spotiflac_android/widgets/mornye_chrome.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
import 'package:spotiflac_android/widgets/custom_font_setting.dart';
class AppearanceSettingsPage extends ConsumerWidget {
const AppearanceSettingsPage({super.key});
@@ -208,6 +209,7 @@ class AppearanceSettingsPage extends ConsumerWidget {
.read(settingsProvider.notifier)
.setHeroAnimationsEnabled(value),
),
const CustomFontSetting(),
SettingsSwitchItem(
icon: Icons.blur_on,
title: context.l10n.appearanceForceBlur,
+96
View File
@@ -0,0 +1,96 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
/// Copies imported fonts into app storage; never persists a picker/container path.
class CustomFontService {
CustomFontService({
Future<Directory> Function()? directory,
Future<void> Function(String, Uint8List)? loadFont,
}) : _directory = directory ?? _fontDirectory,
_loadFont = loadFont ?? _register;
static const maxBytes = 10 * 1024 * 1024;
static final instance = CustomFontService();
final Future<Directory> Function() _directory;
final Future<void> Function(String, Uint8List) _loadFont;
final Map<String, Future<String>> _loads = {};
static final _idPattern = RegExp(r'^[a-f0-9]{64}$');
static Future<Directory> _fontDirectory() async => Directory(
'${(await getApplicationSupportDirectory()).path}/custom_fonts',
).create(recursive: true);
static Future<void> _register(String family, Uint8List bytes) async {
final loader = FontLoader(family)
..addFont(Future.value(ByteData.sublistView(bytes)));
await loader.load();
}
static void validate(Uint8List bytes) {
if (bytes.length < 12 || bytes.length > maxBytes) {
throw const FormatException('Invalid font size');
}
final data = ByteData.sublistView(bytes);
final signature = data.getUint32(0);
if (signature != 0x00010000 && signature != 0x4f54544f) {
throw const FormatException('Expected a TTF or OTF font');
}
final count = data.getUint16(4);
if (count == 0 || 12 + count * 16 > bytes.length) {
throw const FormatException('Invalid font table directory');
}
for (var i = 0; i < count; i++) {
final table = 12 + i * 16;
final offset = data.getUint32(table + 8);
final length = data.getUint32(table + 12);
if (offset > bytes.length || length > bytes.length - offset) {
throw const FormatException('Truncated font table');
}
}
}
Future<String> import(Uint8List bytes) async {
validate(bytes);
final id = sha256.convert(bytes).toString();
await _load(id, bytes);
final directory = await _directory();
final destination = File('${directory.path}/$id.font');
if (!await destination.exists()) {
final staged = await File(
'${directory.path}/$id.tmp',
).writeAsBytes(bytes, flush: true);
await staged.rename(destination.path);
}
return id;
}
Future<String> _load(String id, Uint8List bytes) =>
_loads.putIfAbsent(id, () async {
final family = 'ImportedFont_$id';
try {
await _loadFont(family, bytes);
return family;
} catch (_) {
_loads.remove(id);
rethrow;
}
});
Future<String?> restore(String id) async {
if (!_idPattern.hasMatch(id)) return null;
try {
final file = File('${(await _directory()).path}/$id.font');
if (await file.length() > maxBytes) return null;
final bytes = await file.readAsBytes();
validate(bytes);
if (sha256.convert(bytes).toString() != id) return null;
return await _load(id, bytes);
} catch (_) {
// Missing fonts after restoring preferences fall back to the theme font.
return null;
}
}
}
+4 -2
View File
@@ -31,6 +31,7 @@ class AppTheme {
ColorScheme? dynamicScheme,
Color? seedColor,
bool useSystemFont = false,
String? fontFamily,
}) {
final scheme =
dynamicScheme ??
@@ -61,7 +62,7 @@ class AppTheme {
chipTheme: _chipTheme(scheme),
dividerTheme: _dividerTheme(scheme),
extensions: const <ThemeExtension<dynamic>>[AppTokens.standard],
fontFamily: useSystemFont ? null : 'Google Sans Flex',
fontFamily: fontFamily ?? (useSystemFont ? null : 'Google Sans Flex'),
);
}
@@ -70,6 +71,7 @@ class AppTheme {
Color? seedColor,
bool isAmoled = false,
bool useSystemFont = false,
String? fontFamily,
}) {
final scheme =
dynamicScheme ??
@@ -101,7 +103,7 @@ class AppTheme {
chipTheme: _chipTheme(scheme),
dividerTheme: _dividerTheme(scheme),
extensions: const <ThemeExtension<dynamic>>[AppTokens.standard],
fontFamily: useSystemFont ? null : 'Google Sans Flex',
fontFamily: fontFamily ?? (useSystemFont ? null : 'Google Sans Flex'),
);
}
+6
View File
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:material_ui/material_ui.dart' as material_ui;
import 'package:spotiflac_android/providers/theme_provider.dart';
import 'package:spotiflac_android/providers/custom_font_provider.dart';
import 'package:spotiflac_android/theme/app_theme.dart';
import 'package:spotiflac_android/models/theme_settings.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
@@ -16,6 +17,7 @@ class DynamicColorWrapper extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeSettings = ref.watch(themeProvider);
final fontFamily = ref.watch(customFontFamilyProvider).asData?.value;
if (themeSettings.style == AppThemeStyle.mornye) {
return builder(
@@ -23,6 +25,7 @@ class DynamicColorWrapper extends ConsumerWidget {
Brightness.light,
accent: themeSettings.mornyeAccent,
useSystemFont: themeSettings.useSystemFont,
fontFamily: fontFamily,
glassClarity: themeSettings.mornyeGlassClarity,
glassOutlines: themeSettings.mornyeGlassOutlines,
),
@@ -30,6 +33,7 @@ class DynamicColorWrapper extends ConsumerWidget {
Brightness.dark,
accent: themeSettings.mornyeAccent,
useSystemFont: themeSettings.useSystemFont,
fontFamily: fontFamily,
glassClarity: themeSettings.mornyeGlassClarity,
glassOutlines: themeSettings.mornyeGlassOutlines,
),
@@ -66,11 +70,13 @@ class DynamicColorWrapper extends ConsumerWidget {
final lightTheme = AppTheme.light(
dynamicScheme: lightScheme,
useSystemFont: themeSettings.useSystemFont,
fontFamily: fontFamily,
);
final darkTheme = AppTheme.dark(
dynamicScheme: darkScheme,
isAmoled: themeSettings.useAmoled,
useSystemFont: themeSettings.useSystemFont,
fontFamily: fontFamily,
);
return builder(lightTheme, darkTheme, themeSettings.themeMode);
+23 -5
View File
@@ -13,6 +13,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
this.chromeSurface,
this.accent = MornyeAccent.red,
this.useSystemFont = false,
this.fontFamily,
this.glassClarity = kDefaultMornyeGlassClarity,
this.glassOutlines = true,
});
@@ -20,6 +21,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
final Color? chromeSurface;
final MornyeAccent accent;
final bool useSystemFont;
final String? fontFamily;
final double glassClarity;
final bool glassOutlines;
@@ -118,13 +120,17 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
// many artists cannot grow the cache indefinitely. Platform is part of the
// key because it controls fonts and route transitions.
static final _themeCache =
<(TargetPlatform, Brightness, Color?, MornyeAccent, bool), ThemeData>{};
<
(TargetPlatform, Brightness, Color?, MornyeAccent, bool, String?),
ThemeData
>{};
static ThemeData build(
Brightness brightness, {
Color? chromeSurface,
MornyeAccent accent = MornyeAccent.red,
bool useSystemFont = false,
String? fontFamily,
double glassClarity = kDefaultMornyeGlassClarity,
bool glassOutlines = true,
}) {
@@ -134,6 +140,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
chromeSurface,
accent,
useSystemFont,
fontFamily,
);
final cached = _themeCache.remove(key);
if (cached != null) {
@@ -145,6 +152,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
chromeSurface: chromeSurface,
selectedAccent: accent,
useSystemFont: useSystemFont,
fontFamily: fontFamily,
);
if (_themeCache.length >= 16) {
_themeCache.remove(_themeCache.keys.first);
@@ -190,6 +198,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
chromeSurface: chromeSurface,
accent: preferences?.accent ?? MornyeAccent.red,
useSystemFont: preferences?.useSystemFont ?? false,
fontFamily: preferences?.fontFamily,
glassClarity: preferences?.glassClarity ?? kDefaultMornyeGlassClarity,
glassOutlines: preferences?.glassOutlines ?? true,
);
@@ -200,6 +209,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
Color? chromeSurface,
required MornyeAccent selectedAccent,
required bool useSystemFont,
String? fontFamily,
}) {
final dark = brightness == Brightness.dark;
final accent = accentColor(selectedAccent, brightness);
@@ -251,14 +261,18 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
final text = type.textStyle.copyWith(
inherit: true,
color: foreground,
fontFamily: apple || useSystemFont ? type.textStyle.fontFamily : 'Inter',
fontFamily:
fontFamily ??
(apple || useSystemFont ? type.textStyle.fontFamily : 'Inter'),
);
final display = type.navLargeTitleTextStyle.copyWith(
inherit: true,
color: foreground,
fontFamily: apple || useSystemFont
? type.navLargeTitleTextStyle.fontFamily
: 'Inter',
fontFamily:
fontFamily ??
(apple || useSystemFont
? type.navLargeTitleTextStyle.fontFamily
: 'Inter'),
);
// Supply complete styles: replacing a Material role with a bare TextStyle
// loses its system family and retains Material tracking in other roles.
@@ -312,6 +326,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
chromeSurface: chromeSurface,
accent: selectedAccent,
useSystemFont: useSystemFont,
fontFamily: fontFamily,
),
tokens,
],
@@ -408,12 +423,14 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
Color? chromeSurface,
MornyeAccent? accent,
bool? useSystemFont,
String? fontFamily,
double? glassClarity,
bool? glassOutlines,
}) => MornyeTheme(
chromeSurface: chromeSurface ?? this.chromeSurface,
accent: accent ?? this.accent,
useSystemFont: useSystemFont ?? this.useSystemFont,
fontFamily: fontFamily ?? this.fontFamily,
glassClarity: glassClarity ?? this.glassClarity,
glassOutlines: glassOutlines ?? this.glassOutlines,
);
@@ -422,6 +439,7 @@ class MornyeTheme extends ThemeExtension<MornyeTheme> {
MornyeTheme lerp(covariant MornyeTheme? other, double t) => MornyeTheme(
chromeSurface: Color.lerp(chromeSurface, other?.chromeSurface, t),
accent: t < 0.5 ? accent : other?.accent ?? accent,
fontFamily: t < 0.5 ? fontFamily : other?.fontFamily,
useSystemFont: t < 0.5
? useSystemFont
: other?.useSystemFont ?? useSystemFont,
+72
View File
@@ -0,0 +1,72 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/theme_provider.dart';
import 'package:spotiflac_android/services/custom_font_service.dart';
class CustomFontSetting extends ConsumerStatefulWidget {
const CustomFontSetting({super.key});
@override
ConsumerState<CustomFontSetting> createState() => _CustomFontSettingState();
}
class _CustomFontSettingState extends ConsumerState<CustomFontSetting> {
bool _importing = false;
Future<void> _import() async {
setState(() => _importing = true);
try {
final file = await FilePicker.pickFile(
type: FileType.custom,
allowedExtensions: ['ttf', 'otf'],
);
if (file == null || !mounted) return;
if ((await file.length() ?? 0) > CustomFontService.maxBytes) {
throw const FormatException('Font exceeds import limit');
}
final id = await CustomFontService.instance.import(
await file.readAsBytes(),
);
if (!mounted) return;
await ref.read(themeProvider.notifier).setCustomFont(id, file.name);
} catch (_) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.appearanceCustomFontInvalid)),
);
}
} finally {
if (mounted) setState(() => _importing = false);
}
}
@override
Widget build(BuildContext context) {
final theme = ref.watch(themeProvider);
return ListTile(
leading: const Icon(Icons.text_fields),
title: Text(context.l10n.appearanceCustomFont),
subtitle: Text(
theme.customFontId.isEmpty
? context.l10n.appearanceCustomFontSubtitle
: theme.customFontName,
),
onTap: _importing ? null : _import,
trailing: _importing
? const SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: theme.customFontId.isEmpty
? const Icon(Icons.add)
: IconButton(
tooltip: context.l10n.appearanceCustomFontReset,
onPressed: () =>
ref.read(themeProvider.notifier).setCustomFont('', ''),
icon: const Icon(Icons.close),
),
);
}
}
+114
View File
@@ -0,0 +1,114 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotiflac_android/models/theme_settings.dart';
import 'package:spotiflac_android/providers/theme_provider.dart';
import 'package:spotiflac_android/services/custom_font_service.dart';
import 'package:spotiflac_android/theme/app_theme.dart';
import 'package:spotiflac_android/theme/mornye_theme.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'import persists a private copy, restores after restart, rejects damage',
() async {
final directory = await Directory.systemTemp.createTemp('font-import-');
addTearDown(() => directory.delete(recursive: true));
final bytes = await File('assets/fonts/InterVariable.ttf').readAsBytes();
var loads = 0;
CustomFontService service() => CustomFontService(
directory: () async => directory,
loadFont: (_, data) async {
loads++;
expect(data, bytes);
},
);
final importer = service();
final id = await importer.import(bytes);
expect(await importer.restore(id), 'ImportedFont_$id');
expect(loads, 1);
expect(await service().restore(id), 'ImportedFont_$id');
expect(loads, 2);
await File('${directory.path}/$id.font').writeAsBytes([1, 2]);
expect(await service().restore(id), isNull);
expect(await service().restore('../outside'), isNull);
expect(
() => CustomFontService.validate(Uint8List(12)),
throwsFormatException,
);
expect(
() => CustomFontService.validate(bytes.sublist(0, 32)),
throwsFormatException,
);
},
);
test('font choice survives bootstrap, export and reset', () async {
SharedPreferences.setMockInitialValues({kUseSystemFontKey: true});
final container = ProviderContainer();
addTearDown(container.dispose);
final notifier = container.read(themeProvider.notifier);
await notifier.setCustomFont('selected-font', 'My font.ttf');
final prefs = await SharedPreferences.getInstance();
final theme = loadBootstrapThemeSettings(prefs);
expect(theme.customFontId, 'selected-font');
expect(theme.customFontName, 'My font.ttf');
expect(ThemeSettings.fromJson(theme.toJson()), theme);
await notifier.setCustomFont('', '');
expect(loadBootstrapThemeSettings(prefs).customFontId, isEmpty);
expect(loadBootstrapThemeSettings(prefs).useSystemFont, isTrue);
});
testWidgets(
'custom typography survives Mornye local surface reconstruction',
(tester) async {
final theme = MornyeTheme.build(
Brightness.dark,
fontFamily: 'ImportedFont_test',
);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Builder(
builder: (context) {
final surface = MornyeTheme.fromContext(
context,
chromeSurface: Colors.red,
);
expect(
surface.textTheme.bodyLarge!.fontFamily,
'ImportedFont_test',
);
expect(
surface.textTheme.headlineSmall!.fontFamily,
'ImportedFont_test',
);
return const SizedBox();
},
),
),
);
expect(
AppTheme.light(
fontFamily: 'ImportedFont_test',
).textTheme.bodyMedium!.fontFamily,
'ImportedFont_test',
);
expect(
AppTheme.dark(
fontFamily: 'ImportedFont_test',
).textTheme.bodyMedium!.fontFamily,
'ImportedFont_test',
);
expect(
MornyeTheme.build(Brightness.dark).textTheme.bodyLarge!.fontFamily,
isNot('ImportedFont_test'),
);
},
);
}