appearance.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. import 'dart:async';
  2. import 'package:appflowy/user/application/user_settings_service.dart';
  3. import 'package:appflowy_backend/log.dart';
  4. import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
  5. import 'package:easy_localization/easy_localization.dart';
  6. import 'package:flowy_infra/size.dart';
  7. import 'package:flowy_infra/theme.dart';
  8. import 'package:flowy_infra/theme_extension.dart';
  9. import 'package:flutter/material.dart';
  10. import 'package:flutter_bloc/flutter_bloc.dart';
  11. import 'package:freezed_annotation/freezed_annotation.dart';
  12. part 'appearance.freezed.dart';
  13. const _white = Color(0xFFFFFFFF);
  14. /// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy.
  15. /// It includes the [AppTheme], [ThemeMode], [TextStyles] and [Locale].
  16. class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
  17. final AppearanceSettingsPB _setting;
  18. AppearanceSettingsCubit(AppearanceSettingsPB setting)
  19. : _setting = setting,
  20. super(
  21. AppearanceSettingsState.initial(
  22. setting.theme,
  23. setting.themeMode,
  24. setting.font,
  25. setting.monospaceFont,
  26. setting.locale,
  27. setting.isMenuCollapsed,
  28. setting.menuOffset,
  29. ),
  30. );
  31. /// Update selected theme in the user's settings and emit an updated state
  32. /// with the AppTheme named [themeName].
  33. void setTheme(String themeName) {
  34. _setting.theme = themeName;
  35. _saveAppearanceSettings();
  36. emit(state.copyWith(appTheme: AppTheme.fromName(themeName)));
  37. }
  38. /// Update the theme mode in the user's settings and emit an updated state.
  39. void setThemeMode(ThemeMode themeMode) {
  40. _setting.themeMode = _themeModeToPB(themeMode);
  41. _saveAppearanceSettings();
  42. emit(state.copyWith(themeMode: themeMode));
  43. }
  44. /// Updates the current locale and notify the listeners the locale was
  45. /// changed. Fallback to [en] locale if [newLocale] is not supported.
  46. void setLocale(BuildContext context, Locale newLocale) {
  47. if (!context.supportedLocales.contains(newLocale)) {
  48. Log.warn("Unsupported locale: $newLocale, Fallback to locale: en");
  49. newLocale = const Locale('en');
  50. }
  51. context.setLocale(newLocale).catchError((e) {
  52. Log.warn('Catch error in setLocale: $e}');
  53. });
  54. if (state.locale != newLocale) {
  55. _setting.locale.languageCode = newLocale.languageCode;
  56. _setting.locale.countryCode = newLocale.countryCode ?? "";
  57. _saveAppearanceSettings();
  58. emit(state.copyWith(locale: newLocale));
  59. }
  60. }
  61. // Saves the menus current visibility
  62. void saveIsMenuCollapsed(bool collapsed) {
  63. _setting.isMenuCollapsed = collapsed;
  64. _saveAppearanceSettings();
  65. }
  66. // Saves the current resize offset of the menu
  67. void saveMenuOffset(double offset) {
  68. _setting.menuOffset = offset;
  69. _saveAppearanceSettings();
  70. }
  71. /// Saves key/value setting to disk.
  72. /// Removes the key if the passed in value is null
  73. void setKeyValue(String key, String? value) {
  74. if (key.isEmpty) {
  75. Log.warn("The key should not be empty");
  76. return;
  77. }
  78. if (value == null) {
  79. _setting.settingKeyValue.remove(key);
  80. }
  81. if (_setting.settingKeyValue[key] != value) {
  82. if (value == null) {
  83. _setting.settingKeyValue.remove(key);
  84. } else {
  85. _setting.settingKeyValue[key] = value;
  86. }
  87. }
  88. _saveAppearanceSettings();
  89. }
  90. String? getValue(String key) {
  91. if (key.isEmpty) {
  92. Log.warn("The key should not be empty");
  93. return null;
  94. }
  95. return _setting.settingKeyValue[key];
  96. }
  97. /// Called when the application launches.
  98. /// Uses the device locale when the application is opened for the first time.
  99. void readLocaleWhenAppLaunch(BuildContext context) {
  100. if (_setting.resetToDefault) {
  101. _setting.resetToDefault = false;
  102. _saveAppearanceSettings();
  103. setLocale(context, context.deviceLocale);
  104. return;
  105. }
  106. setLocale(context, state.locale);
  107. }
  108. Future<void> _saveAppearanceSettings() async {
  109. UserSettingsBackendService().setAppearanceSetting(_setting).then((result) {
  110. result.fold(
  111. (l) => null,
  112. (error) => Log.error(error),
  113. );
  114. });
  115. }
  116. }
  117. ThemeMode _themeModeFromPB(ThemeModePB themeModePB) {
  118. switch (themeModePB) {
  119. case ThemeModePB.Light:
  120. return ThemeMode.light;
  121. case ThemeModePB.Dark:
  122. return ThemeMode.dark;
  123. case ThemeModePB.System:
  124. default:
  125. return ThemeMode.system;
  126. }
  127. }
  128. ThemeModePB _themeModeToPB(ThemeMode themeMode) {
  129. switch (themeMode) {
  130. case ThemeMode.light:
  131. return ThemeModePB.Light;
  132. case ThemeMode.dark:
  133. return ThemeModePB.Dark;
  134. case ThemeMode.system:
  135. default:
  136. return ThemeModePB.System;
  137. }
  138. }
  139. @freezed
  140. class AppearanceSettingsState with _$AppearanceSettingsState {
  141. const AppearanceSettingsState._();
  142. const factory AppearanceSettingsState({
  143. required AppTheme appTheme,
  144. required ThemeMode themeMode,
  145. required String font,
  146. required String monospaceFont,
  147. required Locale locale,
  148. required bool isMenuCollapsed,
  149. required double menuOffset,
  150. }) = _AppearanceSettingsState;
  151. factory AppearanceSettingsState.initial(
  152. String themeName,
  153. ThemeModePB themeModePB,
  154. String font,
  155. String monospaceFont,
  156. LocaleSettingsPB localePB,
  157. bool isMenuCollapsed,
  158. double menuOffset,
  159. ) {
  160. return AppearanceSettingsState(
  161. appTheme: AppTheme.fromName(themeName),
  162. font: font,
  163. monospaceFont: monospaceFont,
  164. themeMode: _themeModeFromPB(themeModePB),
  165. locale: Locale(localePB.languageCode, localePB.countryCode),
  166. isMenuCollapsed: isMenuCollapsed,
  167. menuOffset: menuOffset,
  168. );
  169. }
  170. ThemeData get lightTheme => _getThemeData(Brightness.light);
  171. ThemeData get darkTheme => _getThemeData(Brightness.dark);
  172. ThemeData _getThemeData(Brightness brightness) {
  173. // Poppins and SF Mono are not well supported in some languages, so use the
  174. // built-in font for the following languages.
  175. final useBuiltInFontLanguages = [
  176. const Locale('zh', 'CN'),
  177. const Locale('zh', 'TW'),
  178. ];
  179. String fontFamily = font;
  180. String monospaceFontFamily = monospaceFont;
  181. if (useBuiltInFontLanguages.contains(locale)) {
  182. fontFamily = '';
  183. monospaceFontFamily = '';
  184. }
  185. final theme = brightness == Brightness.light
  186. ? appTheme.lightTheme
  187. : appTheme.darkTheme;
  188. final colorScheme = ColorScheme(
  189. brightness: brightness,
  190. primary: theme.primary,
  191. onPrimary: theme.onPrimary,
  192. primaryContainer: theme.main2,
  193. onPrimaryContainer: _white,
  194. // page title hover color
  195. secondary: theme.hoverBG1,
  196. onSecondary: theme.shader1,
  197. // setting value hover color
  198. secondaryContainer: theme.selector,
  199. onSecondaryContainer: theme.topbarBg,
  200. tertiary: theme.shader7,
  201. // Editor: toolbarColor
  202. onTertiary: theme.toolbarColor,
  203. tertiaryContainer: theme.questionBubbleBG,
  204. background: theme.surface,
  205. onBackground: theme.text,
  206. surface: theme.surface,
  207. // text&icon color when it is hovered
  208. onSurface: theme.hoverFG,
  209. // grey hover color
  210. inverseSurface: theme.hoverBG3,
  211. onError: theme.shader7,
  212. error: theme.red,
  213. outline: theme.shader4,
  214. surfaceVariant: theme.sidebarBg,
  215. shadow: theme.shadow,
  216. );
  217. const Set<MaterialState> scrollbarInteractiveStates = <MaterialState>{
  218. MaterialState.pressed,
  219. MaterialState.hovered,
  220. MaterialState.dragged,
  221. };
  222. return ThemeData(
  223. brightness: brightness,
  224. dialogBackgroundColor: theme.surface,
  225. textTheme: _getTextTheme(fontFamily: fontFamily, fontColor: theme.text),
  226. textSelectionTheme: TextSelectionThemeData(
  227. cursorColor: theme.main2,
  228. selectionHandleColor: theme.main2,
  229. ),
  230. iconTheme: IconThemeData(color: theme.icon),
  231. tooltipTheme: TooltipThemeData(
  232. textStyle: _getFontStyle(
  233. fontFamily: fontFamily,
  234. fontSize: FontSizes.s11,
  235. fontWeight: FontWeight.w400,
  236. fontColor: theme.surface,
  237. ),
  238. ),
  239. scaffoldBackgroundColor: theme.surface,
  240. snackBarTheme: SnackBarThemeData(
  241. backgroundColor: colorScheme.primary,
  242. contentTextStyle: TextStyle(color: colorScheme.onSurface),
  243. ),
  244. scrollbarTheme: ScrollbarThemeData(
  245. thumbColor: MaterialStateProperty.resolveWith((states) {
  246. if (states.any(scrollbarInteractiveStates.contains)) {
  247. return theme.shader7;
  248. }
  249. return theme.shader5;
  250. }),
  251. thickness: MaterialStateProperty.resolveWith((states) {
  252. if (states.any(scrollbarInteractiveStates.contains)) {
  253. return 4;
  254. }
  255. return 3.0;
  256. }),
  257. crossAxisMargin: 0.0,
  258. mainAxisMargin: 6.0,
  259. radius: Corners.s10Radius,
  260. ),
  261. materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
  262. //dropdown menu color
  263. canvasColor: theme.surface,
  264. dividerColor: theme.divider,
  265. hintColor: theme.hint,
  266. //action item hover color
  267. hoverColor: theme.hoverBG2,
  268. disabledColor: theme.shader4,
  269. highlightColor: theme.main1,
  270. indicatorColor: theme.main1,
  271. cardColor: theme.input,
  272. colorScheme: colorScheme,
  273. extensions: [
  274. AFThemeExtension(
  275. warning: theme.yellow,
  276. success: theme.green,
  277. tint1: theme.tint1,
  278. tint2: theme.tint2,
  279. tint3: theme.tint3,
  280. tint4: theme.tint4,
  281. tint5: theme.tint5,
  282. tint6: theme.tint6,
  283. tint7: theme.tint7,
  284. tint8: theme.tint8,
  285. tint9: theme.tint9,
  286. textColor: theme.text,
  287. greyHover: theme.hoverBG1,
  288. greySelect: theme.bg3,
  289. lightGreyHover: theme.hoverBG3,
  290. toggleOffFill: theme.shader5,
  291. progressBarBGColor: theme.progressBarBGColor,
  292. toggleButtonBGColor: theme.toggleButtonBGColor,
  293. code: _getFontStyle(
  294. fontFamily: monospaceFontFamily,
  295. fontColor: theme.shader3,
  296. ),
  297. callout: _getFontStyle(
  298. fontFamily: fontFamily,
  299. fontSize: FontSizes.s11,
  300. fontColor: theme.shader3,
  301. ),
  302. caption: _getFontStyle(
  303. fontFamily: fontFamily,
  304. fontSize: FontSizes.s11,
  305. fontWeight: FontWeight.w400,
  306. fontColor: theme.hint,
  307. ),
  308. )
  309. ],
  310. );
  311. }
  312. TextStyle _getFontStyle({
  313. String? fontFamily,
  314. double? fontSize,
  315. FontWeight? fontWeight,
  316. Color? fontColor,
  317. double? letterSpacing,
  318. double? lineHeight,
  319. }) =>
  320. TextStyle(
  321. fontFamily: fontFamily,
  322. fontSize: fontSize ?? FontSizes.s12,
  323. color: fontColor,
  324. fontWeight: fontWeight ?? FontWeight.w500,
  325. fontFamilyFallback: const ["Noto Color Emoji"],
  326. letterSpacing: (fontSize ?? FontSizes.s12) * (letterSpacing ?? 0.005),
  327. height: lineHeight,
  328. );
  329. TextTheme _getTextTheme({
  330. required String fontFamily,
  331. required Color fontColor,
  332. }) {
  333. return TextTheme(
  334. displayLarge: _getFontStyle(
  335. fontFamily: fontFamily,
  336. fontSize: FontSizes.s32,
  337. fontColor: fontColor,
  338. fontWeight: FontWeight.w600,
  339. lineHeight: 42.0,
  340. ), // h2
  341. displayMedium: _getFontStyle(
  342. fontFamily: fontFamily,
  343. fontSize: FontSizes.s24,
  344. fontColor: fontColor,
  345. fontWeight: FontWeight.w600,
  346. lineHeight: 34.0,
  347. ), // h3
  348. displaySmall: _getFontStyle(
  349. fontFamily: fontFamily,
  350. fontSize: FontSizes.s20,
  351. fontColor: fontColor,
  352. fontWeight: FontWeight.w600,
  353. lineHeight: 28.0,
  354. ), // h4
  355. titleLarge: _getFontStyle(
  356. fontFamily: fontFamily,
  357. fontSize: FontSizes.s18,
  358. fontColor: fontColor,
  359. fontWeight: FontWeight.w600,
  360. ), // title
  361. titleMedium: _getFontStyle(
  362. fontFamily: fontFamily,
  363. fontSize: FontSizes.s16,
  364. fontColor: fontColor,
  365. fontWeight: FontWeight.w600,
  366. ), // heading
  367. titleSmall: _getFontStyle(
  368. fontFamily: fontFamily,
  369. fontSize: FontSizes.s14,
  370. fontColor: fontColor,
  371. fontWeight: FontWeight.w600,
  372. ), // subheading
  373. bodyMedium: _getFontStyle(
  374. fontFamily: fontFamily,
  375. fontColor: fontColor,
  376. ), // body-regular
  377. bodySmall: _getFontStyle(
  378. fontFamily: fontFamily,
  379. fontColor: fontColor,
  380. fontWeight: FontWeight.w400,
  381. ), // body-thin
  382. );
  383. }
  384. }