diff --git a/lib/core/error_reporting/crash_report_screen.dart b/lib/core/error_reporting/crash_report_screen.dart index 7ac582a..73771c3 100644 --- a/lib/core/error_reporting/crash_report_screen.dart +++ b/lib/core/error_reporting/crash_report_screen.dart @@ -6,6 +6,7 @@ import 'package:lazurite/core/error_reporting/crash_report_bundle.dart'; import 'package:lazurite/core/l10n/app_localizations.dart'; import 'package:lazurite/core/l10n/l10n.dart'; import 'package:lazurite/core/logging/app_logger.dart'; +import 'package:lazurite/core/theme/theme_extensions.dart'; import 'package:url_launcher/url_launcher.dart'; typedef CrashReportBuilder = Future Function(FlutterErrorDetails details); @@ -229,7 +230,15 @@ class _ReportPreview extends StatelessWidget { children: [ Text(title, style: theme.textTheme.titleSmall), const SizedBox(height: 8), - SelectableText(text, style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace')), + SelectableText( + text, + style: context.codeTextStyle( + fontSize: theme.textTheme.bodySmall?.fontSize ?? 12, + color: theme.textTheme.bodySmall?.color, + letterSpacing: theme.textTheme.bodySmall?.letterSpacing, + height: theme.textTheme.bodySmall?.height, + ), + ), ], ), ), diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 1fd2f86..6c2ea01 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -5,6 +5,7 @@ import 'package:lazurite/core/theme/nord_theme.dart'; import 'package:lazurite/core/theme/oxocarbon_theme.dart'; import 'package:lazurite/core/theme/purple_theme.dart'; import 'package:lazurite/core/theme/rose_pine_theme.dart'; +import 'package:lazurite/core/theme/typography.dart'; enum AppThemePalette { lazurite, oxocarbon, catppuccin, nord, rosePine, purple } @@ -13,21 +14,28 @@ enum AppThemeVariant { light, dark } class AppTheme { AppTheme._(); - static ThemeData getTheme(AppThemePalette palette, AppThemeVariant variant) { - switch (palette) { - case AppThemePalette.lazurite: - return variant == AppThemeVariant.light ? LazuriteTheme.light() : LazuriteTheme.dark(); - case AppThemePalette.oxocarbon: - return variant == AppThemeVariant.light ? OxocarbonTheme.light() : OxocarbonTheme.dark(); - case AppThemePalette.catppuccin: - return variant == AppThemeVariant.light ? CatppuccinTheme.light() : CatppuccinTheme.dark(); - case AppThemePalette.nord: - return variant == AppThemeVariant.light ? NordTheme.light() : NordTheme.dark(); - case AppThemePalette.rosePine: - return variant == AppThemeVariant.light ? RosePineTheme.light() : RosePineTheme.dark(); - case AppThemePalette.purple: - return variant == AppThemeVariant.light ? PurpleTheme.light() : PurpleTheme.dark(); - } + static ThemeData getTheme( + AppThemePalette palette, + AppThemeVariant variant, { + AppHeadingFontFamily headingFontFamily = AppHeadingFontFamily.lora, + AppContentFontFamily contentFontFamily = AppContentFontFamily.googleSans, + AppCodeFontFamily codeFontFamily = AppCodeFontFamily.googleSansCode, + }) { + final theme = switch (palette) { + AppThemePalette.lazurite => variant == AppThemeVariant.light ? LazuriteTheme.light() : LazuriteTheme.dark(), + AppThemePalette.oxocarbon => variant == AppThemeVariant.light ? OxocarbonTheme.light() : OxocarbonTheme.dark(), + AppThemePalette.catppuccin => variant == AppThemeVariant.light ? CatppuccinTheme.light() : CatppuccinTheme.dark(), + AppThemePalette.nord => variant == AppThemeVariant.light ? NordTheme.light() : NordTheme.dark(), + AppThemePalette.rosePine => variant == AppThemeVariant.light ? RosePineTheme.light() : RosePineTheme.dark(), + AppThemePalette.purple => variant == AppThemeVariant.light ? PurpleTheme.light() : PurpleTheme.dark(), + }; + + return AppTypography.applyFontTheme( + theme, + headingFontFamily: headingFontFamily, + contentFontFamily: contentFontFamily, + codeFontFamily: codeFontFamily, + ); } static String getPaletteName(AppThemePalette palette) { diff --git a/lib/core/theme/theme_extensions.dart b/lib/core/theme/theme_extensions.dart index e6f2ccd..edc86f0 100644 --- a/lib/core/theme/theme_extensions.dart +++ b/lib/core/theme/theme_extensions.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lazurite/core/theme/typography.dart'; extension ThemeX on BuildContext { ThemeData get theme => Theme.of(this); @@ -6,4 +7,20 @@ extension ThemeX on BuildContext { ColorScheme get colorScheme => theme.colorScheme; TextTheme get textTheme => theme.textTheme; + + AppFontTheme get fontTheme => theme.extension() ?? AppFontTheme.defaults; + + TextStyle codeTextStyle({ + double fontSize = 14, + FontWeight fontWeight = FontWeight.normal, + Color? color, + double? letterSpacing, + double? height, + }) => fontTheme.codeTextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + letterSpacing: letterSpacing, + height: height, + ); } diff --git a/lib/core/theme/typography.dart b/lib/core/theme/typography.dart index 55dae85..50c8233 100644 --- a/lib/core/theme/typography.dart +++ b/lib/core/theme/typography.dart @@ -1,6 +1,92 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +enum AppHeadingFontFamily { + lora('lora', 'Lora'), + crimsonPro('crimsonPro', 'Crimson Pro'), + playfairDisplay('playfairDisplay', 'Playfair Display'), + merriweather('merriweather', 'Merriweather'), + averiaSerifLibre('averiaSerifLibre', 'Averia Serif Libre'); + + const AppHeadingFontFamily(this.key, this.label); + + final String key; + final String label; +} + +enum AppContentFontFamily { + googleSans('googleSans', 'Google Sans'), + dmSans('dmSans', 'DM Sans'), + publicSans('publicSans', 'Public Sans'), + openSans('openSans', 'Open Sans'); + + const AppContentFontFamily(this.key, this.label); + + final String key; + final String label; +} + +enum AppCodeFontFamily { + googleSansCode('googleSansCode', 'Google Sans Code'), + jetBrainsMono('jetBrainsMono', 'JetBrains Mono'), + firaCode('firaCode', 'Fira Code'), + sourceCodePro('sourceCodePro', 'Source Code Pro'); + + const AppCodeFontFamily(this.key, this.label); + + final String key; + final String label; +} + +@immutable +class AppFontTheme extends ThemeExtension { + const AppFontTheme({required this.headingFontFamily, required this.contentFontFamily, required this.codeFontFamily}); + + static const defaults = AppFontTheme( + headingFontFamily: AppHeadingFontFamily.lora, + contentFontFamily: AppContentFontFamily.googleSans, + codeFontFamily: AppCodeFontFamily.googleSansCode, + ); + + final AppHeadingFontFamily headingFontFamily; + final AppContentFontFamily contentFontFamily; + final AppCodeFontFamily codeFontFamily; + + TextStyle codeTextStyle({ + double fontSize = 14, + FontWeight fontWeight = FontWeight.normal, + Color? color, + double? letterSpacing, + double? height, + }) => AppTypography.code( + codeFontFamily, + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + letterSpacing: letterSpacing, + height: height, + ); + + @override + AppFontTheme copyWith({ + AppHeadingFontFamily? headingFontFamily, + AppContentFontFamily? contentFontFamily, + AppCodeFontFamily? codeFontFamily, + }) => AppFontTheme( + headingFontFamily: headingFontFamily ?? this.headingFontFamily, + contentFontFamily: contentFontFamily ?? this.contentFontFamily, + codeFontFamily: codeFontFamily ?? this.codeFontFamily, + ); + + @override + AppFontTheme lerp(ThemeExtension? other, double t) { + if (other is! AppFontTheme || t < 0.5) { + return this; + } + return other; + } +} + class AppTypography { AppTypography._(); @@ -92,23 +178,322 @@ class AppTypography { ); } - static TextTheme textTheme({Color? bodyColor, Color? headlineColor, Color? captionColor}) { + static TextStyle heading( + AppHeadingFontFamily fontFamily, { + double fontSize = 14, + FontWeight fontWeight = FontWeight.normal, + Color? color, + double? letterSpacing, + double? height, + }) { + return switch (fontFamily) { + AppHeadingFontFamily.lora => _font(GoogleFonts.lora, fontSize, fontWeight, color, letterSpacing, height), + AppHeadingFontFamily.crimsonPro => _font( + GoogleFonts.crimsonPro, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + AppHeadingFontFamily.playfairDisplay => _font( + GoogleFonts.playfairDisplay, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + AppHeadingFontFamily.merriweather => _font( + GoogleFonts.merriweather, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + AppHeadingFontFamily.averiaSerifLibre => _font( + GoogleFonts.averiaSerifLibre, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + }; + } + + static TextStyle content( + AppContentFontFamily fontFamily, { + double fontSize = 14, + FontWeight fontWeight = FontWeight.normal, + Color? color, + double? letterSpacing, + double? height, + }) { + return switch (fontFamily) { + AppContentFontFamily.googleSans => _font( + GoogleFonts.googleSans, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + AppContentFontFamily.dmSans => _font(GoogleFonts.dmSans, fontSize, fontWeight, color, letterSpacing, height), + AppContentFontFamily.publicSans => _font( + GoogleFonts.publicSans, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + AppContentFontFamily.openSans => _font(GoogleFonts.openSans, fontSize, fontWeight, color, letterSpacing, height), + }; + } + + static TextStyle code( + AppCodeFontFamily fontFamily, { + double fontSize = 14, + FontWeight fontWeight = FontWeight.normal, + Color? color, + double? letterSpacing, + double? height, + }) { + return switch (fontFamily) { + AppCodeFontFamily.googleSansCode => _font( + GoogleFonts.googleSansCode, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + AppCodeFontFamily.jetBrainsMono => _font( + GoogleFonts.jetBrainsMono, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + AppCodeFontFamily.firaCode => _font(GoogleFonts.firaCode, fontSize, fontWeight, color, letterSpacing, height), + AppCodeFontFamily.sourceCodePro => _font( + GoogleFonts.sourceCodePro, + fontSize, + fontWeight, + color, + letterSpacing, + height, + ), + }; + } + + static TextTheme textTheme({ + Color? bodyColor, + Color? headlineColor, + Color? captionColor, + AppHeadingFontFamily headingFontFamily = AppHeadingFontFamily.lora, + AppContentFontFamily contentFontFamily = AppContentFontFamily.googleSans, + }) { return TextTheme( - displayLarge: lora(fontSize: 57, fontWeight: FontWeight.w400, color: headlineColor, letterSpacing: -0.25), - displayMedium: lora(fontSize: 45, fontWeight: FontWeight.w400, color: headlineColor), - displaySmall: lora(fontSize: 36, fontWeight: FontWeight.w400, color: headlineColor), - headlineLarge: lora(fontSize: 32, fontWeight: FontWeight.w600, color: headlineColor), - headlineMedium: lora(fontSize: 28, fontWeight: FontWeight.w600, color: headlineColor), - headlineSmall: lora(fontSize: 24, fontWeight: FontWeight.w600, color: headlineColor), - titleLarge: lora(fontSize: 22, fontWeight: FontWeight.w600, color: bodyColor), - titleMedium: lora(fontSize: 16, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.15), - titleSmall: lora(fontSize: 14, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.1), - bodyLarge: googleSans(fontSize: 16, fontWeight: FontWeight.w400, color: bodyColor, letterSpacing: 0.5), - bodyMedium: googleSans(fontSize: 14, fontWeight: FontWeight.w400, color: bodyColor, letterSpacing: 0.25), - bodySmall: googleSans(fontSize: 12, fontWeight: FontWeight.w400, color: captionColor, letterSpacing: 0.4), - labelLarge: googleSans(fontSize: 14, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.1), - labelMedium: googleSans(fontSize: 12, fontWeight: FontWeight.w500, color: bodyColor, letterSpacing: 0.5), - labelSmall: googleSans(fontSize: 12, fontWeight: FontWeight.w500, color: captionColor, letterSpacing: 0.5), + displayLarge: heading( + headingFontFamily, + fontSize: 57, + fontWeight: FontWeight.w400, + color: headlineColor, + letterSpacing: -0.25, + ), + displayMedium: heading(headingFontFamily, fontSize: 45, fontWeight: FontWeight.w400, color: headlineColor), + displaySmall: heading(headingFontFamily, fontSize: 36, fontWeight: FontWeight.w400, color: headlineColor), + headlineLarge: heading(headingFontFamily, fontSize: 32, fontWeight: FontWeight.w600, color: headlineColor), + headlineMedium: heading(headingFontFamily, fontSize: 28, fontWeight: FontWeight.w600, color: headlineColor), + headlineSmall: heading(headingFontFamily, fontSize: 24, fontWeight: FontWeight.w600, color: headlineColor), + titleLarge: heading(headingFontFamily, fontSize: 22, fontWeight: FontWeight.w600, color: bodyColor), + titleMedium: heading( + headingFontFamily, + fontSize: 16, + fontWeight: FontWeight.w500, + color: bodyColor, + letterSpacing: 0.15, + ), + titleSmall: heading( + headingFontFamily, + fontSize: 14, + fontWeight: FontWeight.w500, + color: bodyColor, + letterSpacing: 0.1, + ), + bodyLarge: content( + contentFontFamily, + fontSize: 16, + fontWeight: FontWeight.w400, + color: bodyColor, + letterSpacing: 0.5, + ), + bodyMedium: content( + contentFontFamily, + fontSize: 14, + fontWeight: FontWeight.w400, + color: bodyColor, + letterSpacing: 0.25, + ), + bodySmall: content( + contentFontFamily, + fontSize: 12, + fontWeight: FontWeight.w400, + color: captionColor, + letterSpacing: 0.4, + ), + labelLarge: content( + contentFontFamily, + fontSize: 14, + fontWeight: FontWeight.w500, + color: bodyColor, + letterSpacing: 0.1, + ), + labelMedium: content( + contentFontFamily, + fontSize: 12, + fontWeight: FontWeight.w500, + color: bodyColor, + letterSpacing: 0.5, + ), + labelSmall: content( + contentFontFamily, + fontSize: 12, + fontWeight: FontWeight.w500, + color: captionColor, + letterSpacing: 0.5, + ), + ); + } + + static ThemeData applyFontTheme( + ThemeData theme, { + required AppHeadingFontFamily headingFontFamily, + required AppContentFontFamily contentFontFamily, + required AppCodeFontFamily codeFontFamily, + }) { + final bodyColor = theme.textTheme.bodyMedium?.color ?? theme.colorScheme.onSurface; + final headlineColor = theme.textTheme.headlineMedium?.color ?? theme.colorScheme.onSurface; + final captionColor = theme.textTheme.bodySmall?.color ?? theme.colorScheme.onSurfaceVariant; + final updatedTextTheme = textTheme( + bodyColor: bodyColor, + headlineColor: headlineColor, + captionColor: captionColor, + headingFontFamily: headingFontFamily, + contentFontFamily: contentFontFamily, + ); + final fontTheme = AppFontTheme( + headingFontFamily: headingFontFamily, + contentFontFamily: contentFontFamily, + codeFontFamily: codeFontFamily, + ); + + return theme.copyWith( + textTheme: updatedTextTheme, + extensions: [...theme.extensions.values.where((extension) => extension is! AppFontTheme), fontTheme], + appBarTheme: theme.appBarTheme.copyWith( + titleTextStyle: heading( + headingFontFamily, + fontSize: 18, + fontWeight: FontWeight.w600, + color: theme.appBarTheme.titleTextStyle?.color ?? theme.appBarTheme.foregroundColor, + ), + ), + listTileTheme: theme.listTileTheme.copyWith( + titleTextStyle: content( + contentFontFamily, + fontSize: 16, + fontWeight: FontWeight.w500, + color: theme.listTileTheme.titleTextStyle?.color ?? theme.listTileTheme.textColor, + ), + subtitleTextStyle: content( + contentFontFamily, + fontSize: 14, + color: theme.listTileTheme.subtitleTextStyle?.color ?? theme.colorScheme.onSurfaceVariant, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: theme.elevatedButtonTheme.style?.copyWith( + textStyle: WidgetStatePropertyAll(content(contentFontFamily, fontSize: 14, fontWeight: FontWeight.w500)), + ), + ), + textButtonTheme: TextButtonThemeData( + style: theme.textButtonTheme.style?.copyWith( + textStyle: WidgetStatePropertyAll(content(contentFontFamily, fontSize: 14, fontWeight: FontWeight.w500)), + ), + ), + inputDecorationTheme: theme.inputDecorationTheme.copyWith( + labelStyle: content(contentFontFamily, color: theme.inputDecorationTheme.labelStyle?.color), + hintStyle: content(contentFontFamily, color: theme.inputDecorationTheme.hintStyle?.color), + ), + snackBarTheme: theme.snackBarTheme.copyWith( + contentTextStyle: content(contentFontFamily, color: theme.snackBarTheme.contentTextStyle?.color), + ), + ); + } + + static AppHeadingFontFamily parseHeadingFontFamily(String? value) => AppHeadingFontFamily.values.firstWhere( + (fontFamily) => fontFamily.key == value, + orElse: () => AppHeadingFontFamily.lora, + ); + + static AppContentFontFamily parseContentFontFamily(String? value) => AppContentFontFamily.values.firstWhere( + (fontFamily) => fontFamily.key == value, + orElse: () => AppContentFontFamily.googleSans, + ); + + static AppCodeFontFamily parseCodeFontFamily(String? value) => AppCodeFontFamily.values.firstWhere( + (fontFamily) => fontFamily.key == value, + orElse: () => AppCodeFontFamily.googleSansCode, + ); + + static TextStyle _font( + TextStyle Function({ + Color? backgroundColor, + Color? color, + List? fontFeatures, + double? fontSize, + FontStyle? fontStyle, + FontWeight? fontWeight, + Color? decorationColor, + TextDecoration? decoration, + TextDecorationStyle? decorationStyle, + double? decorationThickness, + List? shadows, + double? height, + Locale? locale, + double? letterSpacing, + TextBaseline? textBaseline, + TextStyle? textStyle, + double? wordSpacing, + }) + fontBuilder, + double fontSize, + FontWeight fontWeight, + Color? color, + double? letterSpacing, + double? height, + ) { + if (_disableFontsForTests) { + return TextStyle( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + letterSpacing: letterSpacing, + height: height, + ); + } + + return fontBuilder( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + letterSpacing: letterSpacing, + height: height, ); } } diff --git a/lib/features/devtools/presentation/dev_tools_screen.dart b/lib/features/devtools/presentation/dev_tools_screen.dart index 4387ddd..281088c 100644 --- a/lib/features/devtools/presentation/dev_tools_screen.dart +++ b/lib/features/devtools/presentation/dev_tools_screen.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lazurite/core/l10n/l10n.dart'; -import 'package:lazurite/core/theme/typography.dart'; import 'package:lazurite/core/theme/theme_extensions.dart'; import 'package:lazurite/core/widgets/app_breadcrumbs.dart'; import 'package:lazurite/features/devtools/cubit/dev_tools_cubit.dart'; @@ -124,7 +123,7 @@ class _SearchInputState extends State<_SearchInput> { contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), isDense: true, ), - style: AppTypography.googleSansCode(fontSize: 13), + style: context.codeTextStyle(fontSize: 13), onChanged: _onQueryChanged, onSubmitted: _resolve, ), @@ -466,7 +465,7 @@ class _CollectionItem extends StatelessWidget { ), child: Icon(_getCollectionIcon(collection.name), size: 16, color: context.colorScheme.primary), ), - title: Text(collection.name, style: AppTypography.googleSansCode(fontSize: 13)), + title: Text(collection.name, style: context.codeTextStyle(fontSize: 13)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -554,7 +553,7 @@ class _RecordsListState extends State<_RecordsList> { Expanded( child: Text( widget.state.selectedCollection ?? '', - style: AppTypography.googleSansCode( + style: context.codeTextStyle( fontSize: context.textTheme.titleSmall?.fontSize ?? 14, fontWeight: context.textTheme.titleSmall?.fontWeight ?? FontWeight.w500, letterSpacing: context.textTheme.titleSmall?.letterSpacing, @@ -607,7 +606,7 @@ class _RecordItem extends StatelessWidget { final preview = _getRecordPreview(record.value); return ListTile( - title: Text(rkey, style: AppTypography.googleSansCode(fontSize: 13)), + title: Text(rkey, style: context.codeTextStyle(fontSize: 13)), subtitle: preview != null ? Text(preview, maxLines: 1, overflow: TextOverflow.ellipsis) : null, trailing: const Icon(Icons.chevron_right), onTap: () => context.read().loadRecord(record), @@ -714,7 +713,7 @@ class _JsonViewer extends StatelessWidget { @override Widget build(BuildContext context) { - return SelectableText(_formatJson(json), style: AppTypography.googleSansCode(fontSize: 12, height: 1.8)); + return SelectableText(_formatJson(json), style: context.codeTextStyle(fontSize: 12, height: 1.8)); } } diff --git a/lib/features/logs/presentation/logs_screen.dart b/lib/features/logs/presentation/logs_screen.dart index 2755fdd..fb6ce45 100644 --- a/lib/features/logs/presentation/logs_screen.dart +++ b/lib/features/logs/presentation/logs_screen.dart @@ -277,7 +277,7 @@ class _SearchBar extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), isDense: true, ), - style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 13, color: context.colorScheme.onSurface), + style: context.codeTextStyle(fontSize: 13, color: context.colorScheme.onSurface), onChanged: (query) => context.read().setSearchQuery(query), ), ); @@ -437,7 +437,7 @@ class _LogEntryTileState extends State<_LogEntryTile> { children: [ Text( widget.entry.formatTimestamp(), - style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, color: context.colorScheme.outline), + style: context.codeTextStyle(fontSize: 11, color: context.colorScheme.outline), ), const SizedBox(width: 8), Container( @@ -445,8 +445,7 @@ class _LogEntryTileState extends State<_LogEntryTile> { decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(3)), child: Text( widget.entry.levelPrefix, - style: TextStyle( - fontFamily: 'JetBrains Mono', + style: context.codeTextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: _isFatalOrError(widget.entry.level) ? Colors.white : levelColor, @@ -460,7 +459,7 @@ class _LogEntryTileState extends State<_LogEntryTile> { children: [ Text( widget.entry.message, - style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 12, color: context.colorScheme.onSurface), + style: context.codeTextStyle(fontSize: 12, color: context.colorScheme.onSurface), maxLines: _expanded ? null : 3, overflow: _expanded ? null : TextOverflow.ellipsis, ), @@ -478,11 +477,7 @@ class _LogEntryTileState extends State<_LogEntryTile> { padding: const EdgeInsets.only(top: 2), child: Text( widget.entry.source!, - style: TextStyle( - fontFamily: 'JetBrains Mono', - fontSize: 11, - color: context.colorScheme.outline, - ), + style: context.codeTextStyle(fontSize: 11, color: context.colorScheme.outline), ), ), ], @@ -570,7 +565,7 @@ class _StackTracePreview extends StatelessWidget { Expanded( child: Text( stackTrace, - style: TextStyle(fontFamily: 'JetBrains Mono', fontSize: 11, color: colorScheme.onSurfaceVariant), + style: context.codeTextStyle(fontSize: 11, color: colorScheme.onSurfaceVariant), maxLines: expanded ? null : 2, overflow: expanded ? null : TextOverflow.ellipsis, ), diff --git a/lib/features/profile/presentation/follow_audit_screen.dart b/lib/features/profile/presentation/follow_audit_screen.dart index f6da3af..d2116de 100644 --- a/lib/features/profile/presentation/follow_audit_screen.dart +++ b/lib/features/profile/presentation/follow_audit_screen.dart @@ -638,9 +638,11 @@ class _ResultRow extends StatelessWidget { _truncateDid(item.record.subjectDid), maxLines: 1, overflow: TextOverflow.ellipsis, - style: context.textTheme.bodySmall?.copyWith( - fontFamily: 'JetBrains Mono', + style: context.codeTextStyle( + fontSize: context.textTheme.bodySmall?.fontSize ?? 12, color: context.colorScheme.onSurfaceVariant, + letterSpacing: context.textTheme.bodySmall?.letterSpacing, + height: context.textTheme.bodySmall?.height, ), ), ), diff --git a/lib/features/search/presentation/semantic_search_tab.dart b/lib/features/search/presentation/semantic_search_tab.dart index 25c4d34..60d2944 100644 --- a/lib/features/search/presentation/semantic_search_tab.dart +++ b/lib/features/search/presentation/semantic_search_tab.dart @@ -315,7 +315,12 @@ class _SemanticSettingsSheet extends StatelessWidget { const Spacer(), Text( '${settingsState.semanticSearchMaxResults}', - style: context.textTheme.titleSmall?.copyWith(fontFamily: 'JetBrains Mono'), + style: context.codeTextStyle( + fontSize: context.textTheme.titleSmall?.fontSize ?? 14, + fontWeight: context.textTheme.titleSmall?.fontWeight ?? FontWeight.w500, + letterSpacing: context.textTheme.titleSmall?.letterSpacing, + height: context.textTheme.titleSmall?.height, + ), ), ], ), @@ -456,10 +461,12 @@ class _RelevanceBadge extends StatelessWidget { decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(99)), child: Text( '${score.round()}%', - style: context.textTheme.labelSmall?.copyWith( + style: context.codeTextStyle( + fontSize: context.textTheme.labelSmall?.fontSize ?? 12, color: color, fontWeight: FontWeight.w600, - fontFamily: 'JetBrains Mono', + letterSpacing: context.textTheme.labelSmall?.letterSpacing, + height: context.textTheme.labelSmall?.height, ), ), ); diff --git a/lib/features/settings/bloc/settings_cubit.dart b/lib/features/settings/bloc/settings_cubit.dart index 5a596ea..e4cf304 100644 --- a/lib/features/settings/bloc/settings_cubit.dart +++ b/lib/features/settings/bloc/settings_cubit.dart @@ -5,6 +5,7 @@ import 'package:lazurite/core/network/app_view_provider.dart'; import 'package:lazurite/core/network/app_view_router.dart'; import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/core/theme/feed_layout.dart'; +import 'package:lazurite/core/theme/typography.dart'; import 'package:lazurite/features/search/data/search_scope.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; @@ -14,6 +15,9 @@ class SettingsCubit extends Cubit { AppThemePalette? initialPalette, AppThemeVariant? initialVariant, bool? initialUseSystemTheme, + AppHeadingFontFamily? initialHeadingFontFamily, + AppContentFontFamily? initialContentFontFamily, + AppCodeFontFamily? initialCodeFontFamily, FeedLayout? initialFeedLayout, bool? initialAnimationsEnabled, bool? initialSimulateOffline, @@ -26,6 +30,9 @@ class SettingsCubit extends Cubit { themePalette: initialPalette ?? AppThemePalette.lazurite, themeVariant: initialVariant ?? AppThemeVariant.dark, useSystemTheme: initialUseSystemTheme ?? false, + headingFontFamily: initialHeadingFontFamily ?? AppHeadingFontFamily.lora, + contentFontFamily: initialContentFontFamily ?? AppContentFontFamily.googleSans, + codeFontFamily: initialCodeFontFamily ?? AppCodeFontFamily.googleSansCode, feedLayout: initialFeedLayout ?? FeedLayout.card, animationsEnabled: initialAnimationsEnabled ?? true, simulateOffline: initialSimulateOffline ?? false, @@ -40,6 +47,9 @@ class SettingsCubit extends Cubit { static const String _keyThemePalette = 'theme_palette'; static const String _keyThemeVariant = 'theme_variant'; static const String _keyUseSystemTheme = 'use_system_theme'; + static const String _keyHeadingFontFamily = 'heading_font_family'; + static const String _keyContentFontFamily = 'content_font_family'; + static const String _keyCodeFontFamily = 'code_font_family'; static const String _keyFeedLayout = 'feed_layout'; static const String _legacyKeyFeedArchitecture = 'feed_architecture'; static const String _keyAnimationsEnabled = 'animations_enabled'; @@ -63,6 +73,9 @@ class SettingsCubit extends Cubit { final paletteStr = await database.getSetting(_keyThemePalette); final variantStr = await database.getSetting(_keyThemeVariant); final useSystemStr = await database.getSetting(_keyUseSystemTheme); + final headingFontFamilyStr = await database.getSetting(_keyHeadingFontFamily); + final contentFontFamilyStr = await database.getSetting(_keyContentFontFamily); + final codeFontFamilyStr = await database.getSetting(_keyCodeFontFamily); final feedLayoutStr = await database.getSetting(_keyFeedLayout) ?? await database.getSetting(_legacyKeyFeedArchitecture); final animationsEnabledStr = await database.getSetting(_keyAnimationsEnabled); @@ -88,6 +101,9 @@ class SettingsCubit extends Cubit { themePalette: AppTheme.parsePalette(paletteStr), themeVariant: AppTheme.parseVariant(variantStr), useSystemTheme: useSystemStr == 'true', + headingFontFamily: AppTypography.parseHeadingFontFamily(headingFontFamilyStr), + contentFontFamily: AppTypography.parseContentFontFamily(contentFontFamilyStr), + codeFontFamily: AppTypography.parseCodeFontFamily(codeFontFamilyStr), feedLayout: FeedLayout.fromString(feedLayoutStr), animationsEnabled: animationsEnabledStr != 'false', simulateOffline: simulateOfflineStr == 'true', @@ -127,6 +143,21 @@ class SettingsCubit extends Cubit { emit(state.copyWith(useSystemTheme: value)); } + Future setHeadingFontFamily(AppHeadingFontFamily fontFamily) async { + await database.setSetting(_keyHeadingFontFamily, fontFamily.key); + emit(state.copyWith(headingFontFamily: fontFamily)); + } + + Future setContentFontFamily(AppContentFontFamily fontFamily) async { + await database.setSetting(_keyContentFontFamily, fontFamily.key); + emit(state.copyWith(contentFontFamily: fontFamily)); + } + + Future setCodeFontFamily(AppCodeFontFamily fontFamily) async { + await database.setSetting(_keyCodeFontFamily, fontFamily.key); + emit(state.copyWith(codeFontFamily: fontFamily)); + } + Future setFeedLayout(FeedLayout layout) async { await database.setSetting(_keyFeedLayout, layout.name); await database.deleteSetting(_legacyKeyFeedArchitecture); diff --git a/lib/features/settings/bloc/settings_state.dart b/lib/features/settings/bloc/settings_state.dart index 5b99080..c82f865 100644 --- a/lib/features/settings/bloc/settings_state.dart +++ b/lib/features/settings/bloc/settings_state.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/core/theme/feed_layout.dart'; +import 'package:lazurite/core/theme/typography.dart'; import 'package:lazurite/features/search/data/search_scope.dart'; const Object _threadAutoCollapseDepthUnset = Object(); @@ -10,6 +11,9 @@ class SettingsState extends Equatable { required this.themePalette, required this.themeVariant, required this.useSystemTheme, + this.headingFontFamily = AppHeadingFontFamily.lora, + this.contentFontFamily = AppContentFontFamily.googleSans, + this.codeFontFamily = AppCodeFontFamily.googleSansCode, this.feedLayout = FeedLayout.card, this.animationsEnabled = true, this.simulateOffline = false, @@ -35,6 +39,9 @@ class SettingsState extends Equatable { final AppThemePalette themePalette; final AppThemeVariant themeVariant; final bool useSystemTheme; + final AppHeadingFontFamily headingFontFamily; + final AppContentFontFamily contentFontFamily; + final AppCodeFontFamily codeFontFamily; final FeedLayout feedLayout; final bool animationsEnabled; final bool simulateOffline; @@ -90,6 +97,9 @@ class SettingsState extends Equatable { AppThemePalette? themePalette, AppThemeVariant? themeVariant, bool? useSystemTheme, + AppHeadingFontFamily? headingFontFamily, + AppContentFontFamily? contentFontFamily, + AppCodeFontFamily? codeFontFamily, FeedLayout? feedLayout, bool? animationsEnabled, bool? simulateOffline, @@ -115,6 +125,9 @@ class SettingsState extends Equatable { themePalette: themePalette ?? this.themePalette, themeVariant: themeVariant ?? this.themeVariant, useSystemTheme: useSystemTheme ?? this.useSystemTheme, + headingFontFamily: headingFontFamily ?? this.headingFontFamily, + contentFontFamily: contentFontFamily ?? this.contentFontFamily, + codeFontFamily: codeFontFamily ?? this.codeFontFamily, feedLayout: feedLayout ?? this.feedLayout, animationsEnabled: animationsEnabled ?? this.animationsEnabled, simulateOffline: simulateOffline ?? this.simulateOffline, @@ -153,6 +166,9 @@ class SettingsState extends Equatable { themePalette, themeVariant, useSystemTheme, + headingFontFamily, + contentFontFamily, + codeFontFamily, feedLayout, animationsEnabled, simulateOffline, diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index eb2c6fa..a73e106 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -13,6 +13,7 @@ import 'package:lazurite/core/network/xrpc_network_interceptor.dart'; import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/core/theme/feed_layout.dart'; import 'package:lazurite/core/theme/theme_extensions.dart'; +import 'package:lazurite/core/theme/typography.dart'; import 'package:lazurite/features/account/cubit/account_switcher_cubit.dart'; import 'package:lazurite/features/account/presentation/account_switcher_sheet.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; @@ -266,6 +267,42 @@ class SettingsScreen extends StatelessWidget { isSelected: state.themePalette == palette, onTap: () => settingsCubit.setThemePalette(palette), ), + const Divider(height: 1), + SettingsDropdownTile( + title: 'Heading Font', + value: state.headingFontFamily, + options: AppHeadingFontFamily.values, + labelBuilder: (fontFamily) => fontFamily.label, + onChanged: (value) { + if (value != null) { + settingsCubit.setHeadingFontFamily(value); + } + }, + ), + const Divider(height: 1), + SettingsDropdownTile( + title: 'Content Font', + value: state.contentFontFamily, + options: AppContentFontFamily.values, + labelBuilder: (fontFamily) => fontFamily.label, + onChanged: (value) { + if (value != null) { + settingsCubit.setContentFontFamily(value); + } + }, + ), + const Divider(height: 1), + SettingsDropdownTile( + title: 'Code Font', + value: state.codeFontFamily, + options: AppCodeFontFamily.values, + labelBuilder: (fontFamily) => fontFamily.label, + onChanged: (value) { + if (value != null) { + settingsCubit.setCodeFontFamily(value); + } + }, + ), const SizedBox(height: 8), ], ), diff --git a/lib/features/settings/presentation/widgets/connection_detail.dart b/lib/features/settings/presentation/widgets/connection_detail.dart index e091001..ed73348 100644 --- a/lib/features/settings/presentation/widgets/connection_detail.dart +++ b/lib/features/settings/presentation/widgets/connection_detail.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lazurite/core/theme/theme_extensions.dart'; class ConnectionDetailRow extends StatelessWidget { const ConnectionDetailRow({super.key, required this.label, required this.value, this.onTap}); @@ -25,7 +26,7 @@ class ConnectionDetailRow extends StatelessWidget { style: theme.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5), ), const SizedBox(height: 4), - Text(value, style: theme.textTheme.bodyMedium?.copyWith(fontFamily: 'JetBrains Mono')), + Text(value, style: context.codeTextStyle().merge(theme.textTheme.bodyMedium)), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index 17e4ef4..169847d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -506,8 +506,20 @@ class _LazuriteAppState extends State with WidgetsBindingObserver { ? ThemeMode.system : (settingsState.themeVariant == AppThemeVariant.light ? ThemeMode.light : ThemeMode.dark); - final lightTheme = AppTheme.getTheme(settingsState.themePalette, AppThemeVariant.light); - final darkTheme = AppTheme.getTheme(settingsState.themePalette, AppThemeVariant.dark); + final lightTheme = AppTheme.getTheme( + settingsState.themePalette, + AppThemeVariant.light, + headingFontFamily: settingsState.headingFontFamily, + contentFontFamily: settingsState.contentFontFamily, + codeFontFamily: settingsState.codeFontFamily, + ); + final darkTheme = AppTheme.getTheme( + settingsState.themePalette, + AppThemeVariant.dark, + headingFontFamily: settingsState.headingFontFamily, + contentFontFamily: settingsState.contentFontFamily, + codeFontFamily: settingsState.codeFontFamily, + ); return MaterialApp.router( key: ValueKey('router-$_routerSessionKey-$_routerGeneration'), diff --git a/test/core/theme/app_theme_test.dart b/test/core/theme/app_theme_test.dart index d75e9b9..ed81000 100644 --- a/test/core/theme/app_theme_test.dart +++ b/test/core/theme/app_theme_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lazurite/core/theme/app_theme.dart'; +import 'package:lazurite/core/theme/typography.dart'; void main() { group('AppTheme', () { @@ -39,6 +40,36 @@ void main() { }); }); + group('getTheme', () { + test('attaches configured font theme extension', () { + final theme = AppTheme.getTheme( + AppThemePalette.lazurite, + AppThemeVariant.dark, + headingFontFamily: AppHeadingFontFamily.playfairDisplay, + contentFontFamily: AppContentFontFamily.openSans, + codeFontFamily: AppCodeFontFamily.sourceCodePro, + ); + + final fontTheme = theme.extension(); + expect(fontTheme, isNotNull); + expect(fontTheme!.headingFontFamily, AppHeadingFontFamily.playfairDisplay); + expect(fontTheme.contentFontFamily, AppContentFontFamily.openSans); + expect(fontTheme.codeFontFamily, AppCodeFontFamily.sourceCodePro); + }); + }); + + group('font parsing', () { + test('parses configured font families and falls back to defaults', () { + expect(AppTypography.parseHeadingFontFamily('merriweather'), AppHeadingFontFamily.merriweather); + expect(AppTypography.parseHeadingFontFamily('averiaSerifLibre'), AppHeadingFontFamily.averiaSerifLibre); + expect(AppTypography.parseHeadingFontFamily('unknown'), AppHeadingFontFamily.lora); + expect(AppTypography.parseContentFontFamily('dmSans'), AppContentFontFamily.dmSans); + expect(AppTypography.parseContentFontFamily('unknown'), AppContentFontFamily.googleSans); + expect(AppTypography.parseCodeFontFamily('firaCode'), AppCodeFontFamily.firaCode); + expect(AppTypography.parseCodeFontFamily('unknown'), AppCodeFontFamily.googleSansCode); + }); + }); + group('parsePalette', () { test('parses lazurite', () { expect(AppTheme.parsePalette('lazurite'), AppThemePalette.lazurite); diff --git a/test/features/settings/bloc/settings_cubit_test.dart b/test/features/settings/bloc/settings_cubit_test.dart index edaccb2..3cdc4f5 100644 --- a/test/features/settings/bloc/settings_cubit_test.dart +++ b/test/features/settings/bloc/settings_cubit_test.dart @@ -6,6 +6,7 @@ import 'package:lazurite/core/network/app_view_fallback_service.dart'; import 'package:lazurite/core/network/app_view_router.dart'; import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/core/theme/feed_layout.dart'; +import 'package:lazurite/core/theme/typography.dart'; import 'package:lazurite/features/settings/bloc/settings_cubit.dart'; import 'package:lazurite/features/settings/bloc/settings_state.dart'; @@ -26,6 +27,9 @@ void main() { expect(cubit.state.themePalette, AppThemePalette.lazurite); expect(cubit.state.themeVariant, AppThemeVariant.dark); expect(cubit.state.useSystemTheme, false); + expect(cubit.state.headingFontFamily, AppHeadingFontFamily.lora); + expect(cubit.state.contentFontFamily, AppContentFontFamily.googleSans); + expect(cubit.state.codeFontFamily, AppCodeFontFamily.googleSansCode); expect(cubit.state.feedLayout, FeedLayout.card); expect(cubit.state.animationsEnabled, true); expect(cubit.state.simulateOffline, false); @@ -44,6 +48,9 @@ void main() { initialPalette: AppThemePalette.catppuccin, initialVariant: AppThemeVariant.light, initialUseSystemTheme: true, + initialHeadingFontFamily: AppHeadingFontFamily.merriweather, + initialContentFontFamily: AppContentFontFamily.publicSans, + initialCodeFontFamily: AppCodeFontFamily.firaCode, initialFeedLayout: FeedLayout.compact, initialAnimationsEnabled: false, initialSimulateOffline: true, @@ -52,6 +59,9 @@ void main() { expect(cubit.state.themePalette, AppThemePalette.catppuccin); expect(cubit.state.themeVariant, AppThemeVariant.light); expect(cubit.state.useSystemTheme, true); + expect(cubit.state.headingFontFamily, AppHeadingFontFamily.merriweather); + expect(cubit.state.contentFontFamily, AppContentFontFamily.publicSans); + expect(cubit.state.codeFontFamily, AppCodeFontFamily.firaCode); expect(cubit.state.feedLayout, FeedLayout.compact); expect(cubit.state.animationsEnabled, false); expect(cubit.state.simulateOffline, true); @@ -65,6 +75,9 @@ void main() { await database.setSetting('theme_palette', 'nord'); await database.setSetting('theme_variant', 'light'); await database.setSetting('use_system_theme', 'true'); + await database.setSetting('heading_font_family', 'crimsonPro'); + await database.setSetting('content_font_family', 'dmSans'); + await database.setSetting('code_font_family', 'sourceCodePro'); await database.setSetting('feed_architecture', 'linear'); await database.setSetting('animations_enabled', 'false'); await database.setSetting('simulate_offline', 'true'); @@ -76,6 +89,9 @@ void main() { .having((s) => s.themePalette, 'themePalette', AppThemePalette.nord) .having((s) => s.themeVariant, 'themeVariant', AppThemeVariant.light) .having((s) => s.useSystemTheme, 'useSystemTheme', true) + .having((s) => s.headingFontFamily, 'headingFontFamily', AppHeadingFontFamily.crimsonPro) + .having((s) => s.contentFontFamily, 'contentFontFamily', AppContentFontFamily.dmSans) + .having((s) => s.codeFontFamily, 'codeFontFamily', AppCodeFontFamily.sourceCodePro) .having((s) => s.feedLayout, 'feedLayout', FeedLayout.compact) .having((s) => s.animationsEnabled, 'animationsEnabled', false) .having((s) => s.simulateOffline, 'simulateOffline', true) @@ -92,6 +108,9 @@ void main() { .having((s) => s.themePalette, 'themePalette', AppThemePalette.lazurite) .having((s) => s.themeVariant, 'themeVariant', AppThemeVariant.dark) .having((s) => s.useSystemTheme, 'useSystemTheme', false) + .having((s) => s.headingFontFamily, 'headingFontFamily', AppHeadingFontFamily.lora) + .having((s) => s.contentFontFamily, 'contentFontFamily', AppContentFontFamily.googleSans) + .having((s) => s.codeFontFamily, 'codeFontFamily', AppCodeFontFamily.googleSansCode) .having((s) => s.feedLayout, 'feedLayout', FeedLayout.card) .having((s) => s.animationsEnabled, 'animationsEnabled', true) .having((s) => s.simulateOffline, 'simulateOffline', false) @@ -153,6 +172,49 @@ void main() { }, ); + blocTest( + 'setHeadingFontFamily updates state and persists to database', + build: () => SettingsCubit(database: database), + act: (cubit) => cubit.setHeadingFontFamily(AppHeadingFontFamily.playfairDisplay), + expect: () => [ + isA().having( + (s) => s.headingFontFamily, + 'headingFontFamily', + AppHeadingFontFamily.playfairDisplay, + ), + ], + verify: (cubit) async { + final value = await database.getSetting('heading_font_family'); + expect(value, 'playfairDisplay'); + }, + ); + + blocTest( + 'setContentFontFamily updates state and persists to database', + build: () => SettingsCubit(database: database), + act: (cubit) => cubit.setContentFontFamily(AppContentFontFamily.openSans), + expect: () => [ + isA().having((s) => s.contentFontFamily, 'contentFontFamily', AppContentFontFamily.openSans), + ], + verify: (cubit) async { + final value = await database.getSetting('content_font_family'); + expect(value, 'openSans'); + }, + ); + + blocTest( + 'setCodeFontFamily updates state and persists to database', + build: () => SettingsCubit(database: database), + act: (cubit) => cubit.setCodeFontFamily(AppCodeFontFamily.firaCode), + expect: () => [ + isA().having((s) => s.codeFontFamily, 'codeFontFamily', AppCodeFontFamily.firaCode), + ], + verify: (cubit) async { + final value = await database.getSetting('code_font_family'); + expect(value, 'firaCode'); + }, + ); + blocTest( 'setFeedLayout updates state and persists to database', build: () => SettingsCubit(database: database), diff --git a/test/features/settings/presentation/settings_screen_test.dart b/test/features/settings/presentation/settings_screen_test.dart index a398302..6a1c434 100644 --- a/test/features/settings/presentation/settings_screen_test.dart +++ b/test/features/settings/presentation/settings_screen_test.dart @@ -11,6 +11,7 @@ import 'package:lazurite/core/database/app_database.dart'; import 'package:lazurite/core/network/app_view_provider.dart'; import 'package:lazurite/core/theme/app_theme.dart'; import 'package:lazurite/core/theme/feed_layout.dart'; +import 'package:lazurite/core/theme/typography.dart'; import 'package:lazurite/features/account/cubit/account_switcher_cubit.dart'; import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; import 'package:lazurite/features/auth/data/models/auth_models.dart'; @@ -59,6 +60,12 @@ void main() { late MockLocalCacheMaintenanceService cacheMaintenanceService; late FakeCrashReportingService crashReportingService; + setUpAll(() { + registerFallbackValue(AppHeadingFontFamily.lora); + registerFallbackValue(AppContentFontFamily.googleSans); + registerFallbackValue(AppCodeFontFamily.googleSansCode); + }); + setUp(() { accountSwitcherCubit = MockAccountSwitcherCubit(); authBloc = MockAuthBloc(); @@ -101,6 +108,9 @@ void main() { ), ); when(() => settingsCubit.setAppViewProvider(any())).thenAnswer((_) async {}); + when(() => settingsCubit.setHeadingFontFamily(any())).thenAnswer((_) async {}); + when(() => settingsCubit.setContentFontFamily(any())).thenAnswer((_) async {}); + when(() => settingsCubit.setCodeFontFamily(any())).thenAnswer((_) async {}); when(() => settingsCubit.refreshAppViewHealth()).thenAnswer((_) async {}); when(() => settingsCubit.setCrashReportingEnabled(any())).thenAnswer((_) async {}); when(() => settingsCubit.setCrashReportingConsentPrompted(any())).thenAnswer((_) async {}); @@ -207,6 +217,12 @@ void main() { expect(find.text('APPEARANCE', skipOffstage: false), findsOneWidget); expect(find.text('System'), findsOneWidget); + expect(find.text('Heading Font'), findsOneWidget); + expect(find.text('Content Font'), findsOneWidget); + expect(find.text('Code Font'), findsOneWidget); + expect(find.text('Lora'), findsOneWidget); + expect(find.text('Google Sans'), findsWidgets); + expect(find.text('Google Sans Code'), findsOneWidget); await tester.scrollUntilVisible(find.text('Feed Layout'), 300); await tester.pumpAndSettle(); @@ -217,6 +233,30 @@ void main() { expect(find.text('Animations'), findsOneWidget); }); + testWidgets('appearance font dropdowns update settings cubit', (tester) async { + await tester.pumpWidget(buildSubject()); + await tester.pumpAndSettle(); + + final headingDropdown = tester.widget>( + find.byType(DropdownButton), + ); + headingDropdown.onChanged?.call(AppHeadingFontFamily.merriweather); + + final contentDropdown = tester.widget>( + find.byType(DropdownButton), + ); + contentDropdown.onChanged?.call(AppContentFontFamily.openSans); + + final codeDropdown = tester.widget>( + find.byType(DropdownButton), + ); + codeDropdown.onChanged?.call(AppCodeFontFamily.sourceCodePro); + + verify(() => settingsCubit.setHeadingFontFamily(AppHeadingFontFamily.merriweather)).called(1); + verify(() => settingsCubit.setContentFontFamily(AppContentFontFamily.openSans)).called(1); + verify(() => settingsCubit.setCodeFontFamily(AppCodeFontFamily.sourceCodePro)).called(1); + }); + testWidgets('renders native app version and build number', (tester) async { await tester.pumpWidget(buildSubject()); await tester.pumpAndSettle();