mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-10-31 10:03:18 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			446 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			446 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| 
 | |
| import 'package:appflowy/user/application/user_settings_service.dart';
 | |
| import 'package:appflowy_backend/log.dart';
 | |
| import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
 | |
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flowy_infra/size.dart';
 | |
| import 'package:flowy_infra/theme.dart';
 | |
| import 'package:flowy_infra/theme_extension.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_bloc/flutter_bloc.dart';
 | |
| import 'package:freezed_annotation/freezed_annotation.dart';
 | |
| import 'package:google_fonts/google_fonts.dart';
 | |
| 
 | |
| part 'appearance.freezed.dart';
 | |
| 
 | |
| const _white = Color(0xFFFFFFFF);
 | |
| 
 | |
| /// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy.
 | |
| /// It includes the [AppTheme], [ThemeMode], [TextStyles] and [Locale].
 | |
| class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
 | |
|   final AppearanceSettingsPB _setting;
 | |
| 
 | |
|   AppearanceSettingsCubit(AppearanceSettingsPB setting)
 | |
|       : _setting = setting,
 | |
|         super(
 | |
|           AppearanceSettingsState.initial(
 | |
|             setting.theme,
 | |
|             setting.themeMode,
 | |
|             setting.font,
 | |
|             setting.monospaceFont,
 | |
|             setting.locale,
 | |
|             setting.isMenuCollapsed,
 | |
|             setting.menuOffset,
 | |
|           ),
 | |
|         );
 | |
| 
 | |
|   /// Update selected theme in the user's settings and emit an updated state
 | |
|   /// with the AppTheme named [themeName].
 | |
|   Future<void> setTheme(String themeName) async {
 | |
|     _setting.theme = themeName;
 | |
|     _saveAppearanceSettings();
 | |
|     emit(state.copyWith(appTheme: await AppTheme.fromName(themeName)));
 | |
|   }
 | |
| 
 | |
|   /// Update the theme mode in the user's settings and emit an updated state.
 | |
|   void setThemeMode(ThemeMode themeMode) {
 | |
|     _setting.themeMode = _themeModeToPB(themeMode);
 | |
|     _saveAppearanceSettings();
 | |
|     emit(state.copyWith(themeMode: themeMode));
 | |
|   }
 | |
| 
 | |
|   /// Toggle the theme mode
 | |
|   void toggleThemeMode() {
 | |
|     final currentThemeMode = state.themeMode;
 | |
|     setThemeMode(
 | |
|       currentThemeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /// Update selected font in the user's settings and emit an updated state
 | |
|   /// with the font name.
 | |
|   void setFontFamily(String fontFamilyName) {
 | |
|     _setting.font = fontFamilyName;
 | |
|     _saveAppearanceSettings();
 | |
|     emit(state.copyWith(font: fontFamilyName));
 | |
|   }
 | |
| 
 | |
|   /// Updates the current locale and notify the listeners the locale was
 | |
|   /// changed. Fallback to [en] locale if [newLocale] is not supported.
 | |
|   void setLocale(BuildContext context, Locale newLocale) {
 | |
|     if (!context.supportedLocales.contains(newLocale)) {
 | |
|       // Log.warn("Unsupported locale: $newLocale, Fallback to locale: en");
 | |
|       newLocale = const Locale('en');
 | |
|     }
 | |
| 
 | |
|     context.setLocale(newLocale).catchError((e) {
 | |
|       Log.warn('Catch error in setLocale: $e}');
 | |
|     });
 | |
| 
 | |
|     if (state.locale != newLocale) {
 | |
|       _setting.locale.languageCode = newLocale.languageCode;
 | |
|       _setting.locale.countryCode = newLocale.countryCode ?? "";
 | |
|       _saveAppearanceSettings();
 | |
|       emit(state.copyWith(locale: newLocale));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Saves the menus current visibility
 | |
|   void saveIsMenuCollapsed(bool collapsed) {
 | |
|     _setting.isMenuCollapsed = collapsed;
 | |
|     _saveAppearanceSettings();
 | |
|   }
 | |
| 
 | |
|   // Saves the current resize offset of the menu
 | |
|   void saveMenuOffset(double offset) {
 | |
|     _setting.menuOffset = offset;
 | |
|     _saveAppearanceSettings();
 | |
|   }
 | |
| 
 | |
|   /// Saves key/value setting to disk.
 | |
|   /// Removes the key if the passed in value is null
 | |
|   void setKeyValue(String key, String? value) {
 | |
|     if (key.isEmpty) {
 | |
|       Log.warn("The key should not be empty");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (value == null) {
 | |
|       _setting.settingKeyValue.remove(key);
 | |
|     }
 | |
| 
 | |
|     if (_setting.settingKeyValue[key] != value) {
 | |
|       if (value == null) {
 | |
|         _setting.settingKeyValue.remove(key);
 | |
|       } else {
 | |
|         _setting.settingKeyValue[key] = value;
 | |
|       }
 | |
|     }
 | |
|     _saveAppearanceSettings();
 | |
|   }
 | |
| 
 | |
|   String? getValue(String key) {
 | |
|     if (key.isEmpty) {
 | |
|       Log.warn("The key should not be empty");
 | |
|       return null;
 | |
|     }
 | |
|     return _setting.settingKeyValue[key];
 | |
|   }
 | |
| 
 | |
|   /// Called when the application launches.
 | |
|   /// Uses the device locale when the application is opened for the first time.
 | |
|   void readLocaleWhenAppLaunch(BuildContext context) {
 | |
|     if (_setting.resetToDefault) {
 | |
|       _setting.resetToDefault = false;
 | |
|       _saveAppearanceSettings();
 | |
|       setLocale(context, context.deviceLocale);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     setLocale(context, state.locale);
 | |
|   }
 | |
| 
 | |
|   Future<void> _saveAppearanceSettings() async {
 | |
|     UserSettingsBackendService().setAppearanceSetting(_setting).then((result) {
 | |
|       result.fold(
 | |
|         (l) => null,
 | |
|         (error) => Log.error(error),
 | |
|       );
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| ThemeMode _themeModeFromPB(ThemeModePB themeModePB) {
 | |
|   switch (themeModePB) {
 | |
|     case ThemeModePB.Light:
 | |
|       return ThemeMode.light;
 | |
|     case ThemeModePB.Dark:
 | |
|       return ThemeMode.dark;
 | |
|     case ThemeModePB.System:
 | |
|     default:
 | |
|       return ThemeMode.system;
 | |
|   }
 | |
| }
 | |
| 
 | |
| ThemeModePB _themeModeToPB(ThemeMode themeMode) {
 | |
|   switch (themeMode) {
 | |
|     case ThemeMode.light:
 | |
|       return ThemeModePB.Light;
 | |
|     case ThemeMode.dark:
 | |
|       return ThemeModePB.Dark;
 | |
|     case ThemeMode.system:
 | |
|     default:
 | |
|       return ThemeModePB.System;
 | |
|   }
 | |
| }
 | |
| 
 | |
| @freezed
 | |
| class AppearanceSettingsState with _$AppearanceSettingsState {
 | |
|   const AppearanceSettingsState._();
 | |
| 
 | |
|   const factory AppearanceSettingsState({
 | |
|     required AppTheme appTheme,
 | |
|     required ThemeMode themeMode,
 | |
|     required String font,
 | |
|     required String monospaceFont,
 | |
|     required Locale locale,
 | |
|     required bool isMenuCollapsed,
 | |
|     required double menuOffset,
 | |
|   }) = _AppearanceSettingsState;
 | |
| 
 | |
|   factory AppearanceSettingsState.initial(
 | |
|     String themeName,
 | |
|     ThemeModePB themeModePB,
 | |
|     String font,
 | |
|     String monospaceFont,
 | |
|     LocaleSettingsPB localePB,
 | |
|     bool isMenuCollapsed,
 | |
|     double menuOffset,
 | |
|   ) {
 | |
|     return AppearanceSettingsState(
 | |
|       appTheme: AppTheme.fallback,
 | |
|       font: font,
 | |
|       monospaceFont: monospaceFont,
 | |
|       themeMode: _themeModeFromPB(themeModePB),
 | |
|       locale: Locale(localePB.languageCode, localePB.countryCode),
 | |
|       isMenuCollapsed: isMenuCollapsed,
 | |
|       menuOffset: menuOffset,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   ThemeData get lightTheme => _getThemeData(Brightness.light);
 | |
|   ThemeData get darkTheme => _getThemeData(Brightness.dark);
 | |
| 
 | |
|   ThemeData _getThemeData(Brightness brightness) {
 | |
|     // Poppins and SF Mono are not well supported in some languages, so use the
 | |
|     // built-in font for the following languages.
 | |
|     final useBuiltInFontLanguages = [
 | |
|       const Locale('zh', 'CN'),
 | |
|       const Locale('zh', 'TW'),
 | |
|     ];
 | |
|     String fontFamily = font;
 | |
|     String monospaceFontFamily = monospaceFont;
 | |
|     if (useBuiltInFontLanguages.contains(locale)) {
 | |
|       fontFamily = '';
 | |
|       monospaceFontFamily = '';
 | |
|     }
 | |
| 
 | |
|     final theme = brightness == Brightness.light
 | |
|         ? appTheme.lightTheme
 | |
|         : appTheme.darkTheme;
 | |
| 
 | |
|     final colorScheme = ColorScheme(
 | |
|       brightness: brightness,
 | |
|       primary: theme.primary,
 | |
|       onPrimary: theme.onPrimary,
 | |
|       primaryContainer: theme.main2,
 | |
|       onPrimaryContainer: _white,
 | |
|       // page title hover color
 | |
|       secondary: theme.hoverBG1,
 | |
|       onSecondary: theme.shader1,
 | |
|       // setting value hover color
 | |
|       secondaryContainer: theme.selector,
 | |
|       onSecondaryContainer: theme.topbarBg,
 | |
|       tertiary: theme.shader7,
 | |
|       // Editor: toolbarColor
 | |
|       onTertiary: theme.toolbarColor,
 | |
|       tertiaryContainer: theme.questionBubbleBG,
 | |
|       background: theme.surface,
 | |
|       onBackground: theme.text,
 | |
|       surface: theme.surface,
 | |
|       // text&icon color when it is hovered
 | |
|       onSurface: theme.hoverFG,
 | |
|       // grey hover color
 | |
|       inverseSurface: theme.hoverBG3,
 | |
|       onError: theme.shader7,
 | |
|       error: theme.red,
 | |
|       outline: theme.shader4,
 | |
|       surfaceVariant: theme.sidebarBg,
 | |
|       shadow: theme.shadow,
 | |
|     );
 | |
| 
 | |
|     const Set<MaterialState> scrollbarInteractiveStates = <MaterialState>{
 | |
|       MaterialState.pressed,
 | |
|       MaterialState.hovered,
 | |
|       MaterialState.dragged,
 | |
|     };
 | |
| 
 | |
|     return ThemeData(
 | |
|       brightness: brightness,
 | |
|       dialogBackgroundColor: theme.surface,
 | |
|       textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text),
 | |
|       textSelectionTheme: TextSelectionThemeData(
 | |
|         cursorColor: theme.main2,
 | |
|         selectionHandleColor: theme.main2,
 | |
|       ),
 | |
|       iconTheme: IconThemeData(color: theme.icon),
 | |
|       tooltipTheme: TooltipThemeData(
 | |
|         textStyle: _getFontStyle(
 | |
|           fontFamily: fontFamily,
 | |
|           fontSize: FontSizes.s11,
 | |
|           fontWeight: FontWeight.w400,
 | |
|           fontColor: theme.surface,
 | |
|         ),
 | |
|       ),
 | |
|       scaffoldBackgroundColor: theme.surface,
 | |
|       snackBarTheme: SnackBarThemeData(
 | |
|         backgroundColor: colorScheme.primary,
 | |
|         contentTextStyle: TextStyle(color: colorScheme.onSurface),
 | |
|       ),
 | |
|       scrollbarTheme: ScrollbarThemeData(
 | |
|         thumbColor: MaterialStateProperty.resolveWith((states) {
 | |
|           if (states.any(scrollbarInteractiveStates.contains)) {
 | |
|             return theme.shader7;
 | |
|           }
 | |
|           return theme.shader5;
 | |
|         }),
 | |
|         thickness: MaterialStateProperty.resolveWith((states) {
 | |
|           if (states.any(scrollbarInteractiveStates.contains)) {
 | |
|             return 4;
 | |
|           }
 | |
|           return 3.0;
 | |
|         }),
 | |
|         crossAxisMargin: 0.0,
 | |
|         mainAxisMargin: 6.0,
 | |
|         radius: Corners.s10Radius,
 | |
|       ),
 | |
|       materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
 | |
|       //dropdown menu color
 | |
|       canvasColor: theme.surface,
 | |
|       dividerColor: theme.divider,
 | |
|       hintColor: theme.hint,
 | |
|       //action item hover color
 | |
|       hoverColor: theme.hoverBG2,
 | |
|       disabledColor: theme.shader4,
 | |
|       highlightColor: theme.main1,
 | |
|       indicatorColor: theme.main1,
 | |
|       cardColor: theme.input,
 | |
|       colorScheme: colorScheme,
 | |
|       extensions: [
 | |
|         AFThemeExtension(
 | |
|           warning: theme.yellow,
 | |
|           success: theme.green,
 | |
|           tint1: theme.tint1,
 | |
|           tint2: theme.tint2,
 | |
|           tint3: theme.tint3,
 | |
|           tint4: theme.tint4,
 | |
|           tint5: theme.tint5,
 | |
|           tint6: theme.tint6,
 | |
|           tint7: theme.tint7,
 | |
|           tint8: theme.tint8,
 | |
|           tint9: theme.tint9,
 | |
|           textColor: theme.text,
 | |
|           greyHover: theme.hoverBG1,
 | |
|           greySelect: theme.bg3,
 | |
|           lightGreyHover: theme.hoverBG3,
 | |
|           toggleOffFill: theme.shader5,
 | |
|           progressBarBGColor: theme.progressBarBGColor,
 | |
|           toggleButtonBGColor: theme.toggleButtonBGColor,
 | |
|           code: _getFontStyle(
 | |
|             fontFamily: monospaceFontFamily,
 | |
|             fontColor: theme.shader3,
 | |
|           ),
 | |
|           callout: _getFontStyle(
 | |
|             fontFamily: fontFamily,
 | |
|             fontSize: FontSizes.s11,
 | |
|             fontColor: theme.shader3,
 | |
|           ),
 | |
|           caption: _getFontStyle(
 | |
|             fontFamily: fontFamily,
 | |
|             fontSize: FontSizes.s11,
 | |
|             fontWeight: FontWeight.w400,
 | |
|             fontColor: theme.hint,
 | |
|           ),
 | |
|         )
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   TextStyle _getFontStyle({
 | |
|     required String fontFamily,
 | |
|     double? fontSize,
 | |
|     FontWeight? fontWeight,
 | |
|     Color? fontColor,
 | |
|     double? letterSpacing,
 | |
|     double? lineHeight,
 | |
|   }) {
 | |
|     try {
 | |
|       return GoogleFonts.getFont(
 | |
|         fontFamily,
 | |
|         fontSize: fontSize ?? FontSizes.s12,
 | |
|         color: fontColor,
 | |
|         fontWeight: fontWeight ?? FontWeight.w500,
 | |
|         letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
 | |
|         height: lineHeight,
 | |
|       );
 | |
|     } catch (e) {
 | |
|       return TextStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontSize: fontSize ?? FontSizes.s12,
 | |
|         color: fontColor,
 | |
|         fontWeight: fontWeight ?? FontWeight.w500,
 | |
|         fontFamilyFallback: const ["Noto Color Emoji"],
 | |
|         letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
 | |
|         height: lineHeight,
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   TextTheme _getTextTheme({
 | |
|     required String fontFamily,
 | |
|     required Color fontColor,
 | |
|   }) {
 | |
|     return TextTheme(
 | |
|       displayLarge: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontSize: FontSizes.s32,
 | |
|         fontColor: fontColor,
 | |
|         fontWeight: FontWeight.w600,
 | |
|         lineHeight: 42.0,
 | |
|       ), // h2
 | |
|       displayMedium: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontSize: FontSizes.s24,
 | |
|         fontColor: fontColor,
 | |
|         fontWeight: FontWeight.w600,
 | |
|         lineHeight: 34.0,
 | |
|       ), // h3
 | |
|       displaySmall: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontSize: FontSizes.s20,
 | |
|         fontColor: fontColor,
 | |
|         fontWeight: FontWeight.w600,
 | |
|         lineHeight: 28.0,
 | |
|       ), // h4
 | |
|       titleLarge: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontSize: FontSizes.s18,
 | |
|         fontColor: fontColor,
 | |
|         fontWeight: FontWeight.w600,
 | |
|       ), // title
 | |
|       titleMedium: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontSize: FontSizes.s16,
 | |
|         fontColor: fontColor,
 | |
|         fontWeight: FontWeight.w600,
 | |
|       ), // heading
 | |
|       titleSmall: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontSize: FontSizes.s14,
 | |
|         fontColor: fontColor,
 | |
|         fontWeight: FontWeight.w600,
 | |
|       ), // subheading
 | |
|       bodyMedium: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontColor: fontColor,
 | |
|       ), // body-regular
 | |
|       bodySmall: _getFontStyle(
 | |
|         fontFamily: fontFamily,
 | |
|         fontColor: fontColor,
 | |
|         fontWeight: FontWeight.w400,
 | |
|       ), // body-thin
 | |
|     );
 | |
|   }
 | |
| }
 | 
